Check a JSON payload against a JSON Schema, Zod or Pydantic model with every error marked in place, and convert a schema between all three.
How it works
Validation puts a schema beside a JSON payload and checks the payload as you type. Each problem is listed with its JSON Pointer and a sentence, and marked in the payload: on the key when the key is at fault, such as a property the schema does not allow, and on the value otherwise. Clicking a problem selects it in the editor.
Conversion puts one schema language beside another; all three are read into one intermediate form and written out of it. Pydantic needs a class per object, so an inline object becomes a class named after its field, and enums become Literal types. Zod is written in Zod 4's style, z.email() rather than z.string().email(), though both are read.
format is checked rather than treated as an annotation: email, date, date-time, time, uuid, ipv4, ipv6, uri, hostname and a few more, with 2023-02-29 refused as a date. String lengths count code points, so an emoji is one character, and multipleOf: 0.1 accepts 0.3 despite floating point. When every branch of a union fails and only one of them could have fitted the value's type, that branch's own errors are reported.
With the schema first, Generate payload writes a skeleton the schema accepts the shape of: empty strings and zeros, defaults and first enum values, nested objects built out. Swap puts the payload first, and the button then writes a schema inferred from it, every key required and string formats detected, for you to loosen.
Examples
A payload against a Pydantic model
class Address(BaseModel):
postcode: str = Field(pattern="^[0-9]{4,6}$")
class User(BaseModel):
email: EmailStr
age: int = Field(ge=0)
address: Optional[Address] = None
{
"email": "ada.example.com",
"age": 36.5,
"address": { "postcode": "SW1Y" }
}
| Pointer | Problem |
|---|---|
/email | Must be a valid email address |
/age | Must be a whole number, found 36.5 |
/address/postcode | Must match /^[0-9]{4,6}$/ |
address may be an Address or null. The value is an object, so only the Address branch could fit, and its complaint about the postcode is the one shown. Given 5 instead, it says “Expected Address or null, found an integer”.
Zod to Pydantic
export const User = z.object({
id: z.uuid(),
role: z.enum(["admin", "viewer"]).default("viewer"),
nickname: z.string().optional(),
address: z.object({ city: z.string().min(1) }),
});
from typing import Literal, Optional
from uuid import UUID
from pydantic import BaseModel, Field
class UserAddress(BaseModel):
city: str = Field(..., min_length=1)
class User(BaseModel):
id: UUID
role: Literal["admin", "viewer"] = "viewer"
nickname: Optional[str] = None
address: UserAddress
A schema inferred from records
[
{ "id": "5f1c2b9e-8a41-4d3a-9c6e-2f0b7d8e1a34", "email": "ada@example.com", "score": 9 },
{ "id": "0b7d8e1a-2f0b-4d3a-9c6e-5f1c2b9e8a41", "email": "grace@example.com", "score": 7.5, "team": null }
]
export const Root = z.array(z.object({
id: z.uuid(),
email: z.email(),
score: z.number(),
team: z.null().optional(),
}));
Every element is read and the readings merged: 9 and 7.5 make a number rather than an integer, and team, present in one record only, becomes optional.
Common problems
.optional() and Optional are not the same
In Zod, .optional() means the key may be missing. In Python, Optional[str] means the value may be null, and a model has no way to say a key may be absent. So a property that is neither required nor given a default becomes Optional[str] = None in Pydantic, as nickname does above, and accepts null where the Zod schema did not.
.refine() disappears
A refinement is a function, and neither JSON Schema nor a Pydantic field has anywhere to put one. It is left out of the conversion and of validation, and listed under The schema as “.refine() is a rule written in code, so it is left out of the conversion”.
A $ref to another file
Only references inside the document, #/$defs/Name or #/definitions/Name, are followed. Nothing is fetched, so a reference to https://example.com/address.json is reported as pointing outside the document and allows any value in the meantime.
The payload is not quite JSON
The payload is read as strict JSON, so a trailing comma gives “Line 1, column 9 — Expected a property name in double quotes” and nothing is validated until it is fixed. Repair on the JSON page removes trailing commas and comments and requotes single-quoted strings.
Frequently asked questions
Which JSON Schema draft does it use?
It writes draft 2020-12. It reads the 2020-12 keywords and the draft-07 spellings still common in older schemas: definitions beside $defs, and items as an array for a tuple beside prefixItems.
Which keywords are checked?
Types, enum, const, properties, required, additionalProperties, patternProperties, propertyNames patterns, minProperties, maxProperties, dependentRequired, items, prefixItems, contains with minContains and maxContains, the length and item counts, uniqueItems, the numeric bounds, multipleOf, pattern, format, allOf, anyOf, oneOf as exactly one, not, if/then/else and local $ref. All of them survive a conversion to JSON Schema. Zod and Pydantic are written without not, if, contains, patternProperties, the property counts or dependentRequired, and with oneOf as a plain union, and a note under The schema says so for each one the schema uses.
Is my Zod or Pydantic code run?
No. It is parsed as source with the same grammar the editor highlights it with, and read as written. A schema built at run time, such as z.object(makeShape()), cannot be followed and is reported as such: “The shape of an object schema has to be written out in the file”.