How type inference works
The generator walks every value in your example and maps it to the closest JSON Schema type. The mapping is deterministic: whole numbers become integer, decimals become number, quoted values become string, true/false become boolean, null becomes null, and the composite types array and object recurse into their contents.
{ "id": 42, "score": 3.14, "name": "Ada", "active": true, "deleted": null }{
"type": "object",
"properties": {
"id": { "type": "integer" },
"score": { "type": "number" },
"name": { "type": "string" },
"active": { "type": "boolean" },
"deleted": { "type": "null" }
}
}Note that 42 infers integer, not number. If that field can also hold a decimal in real data, widen it to number by hand — the example alone can't tell.
How required fields are determined
A single example only reveals which keys are present, so every present key is added to the required array by default. The example cannot signal which fields are optional in production — that's intent, and inference never recovers intent. This is the most common source of false validation failures: a field that's optional in the API but happened to appear in your example gets marked required.
{ "id": 1, "name": "Ada", "nickname": "Countess" }
// generated:
// "required": ["id", "name", "nickname"]
// but if nickname is optional, trim it:
// "required": ["id", "name"]Uncheck the required toggle to omit the array entirely, then add back only the fields your producer truly guarantees. Remember required applies per-object, so nested objects each carry their own list.
Nested objects become nested properties
Each nested object turns into its own object schema with its own properties and required. Nesting recurses to any depth, and because required is per-object, the inner object's required list is independent of the outer one.
{ "user": { "name": "Ada", "address": { "city": "London" } } }{
"type": "object",
"properties": {
"user": {
"type": "object",
"properties": {
"name": { "type": "string" },
"address": {
"type": "object",
"properties": { "city": { "type": "string" } },
"required": ["city"]
}
},
"required": ["name", "address"]
}
},
"required": ["user"]
}Arrays, items, and heterogeneous elements
An array becomes { "type": "array", "items": ... }. When every element shares a type, items is a single schema. When elements differ, the generator emits an anyOf union — usually worth investigating, since a mixed array is often a bug. An empty array [] produces type: array with no items, because the element type is unknowable from the example.
{ "tags": ["a", "b"], "mixed": [1, "x"], "empty": [] }"tags": { "type": "array", "items": { "type": "string" } }
"mixed": { "type": "array", "items": { "anyOf": [
{ "type": "integer" }, { "type": "string" } ] } }
"empty": { "type": "array" }For fixed-position tuples (e.g. [lng, lat]), draft 2020-12's prefixItems is the right tool — but you add it by hand, since inference can't distinguish a tuple from a homogeneous list.
Choosing a draft: draft-07 vs 2020-12
The generator declares the chosen draft in the $schema header so validators interpret keywords correctly. The two offered drafts differ in a few keywords that matter once you start refining:
// draft-07 — widest compatibility (Ajv, Python jsonschema, most tooling)
"$schema": "http://json-schema.org/draft-07/schema#"
// shared subschemas live under "definitions"
// 2020-12 — latest standard
"$schema": "https://json-schema.org/draft/2020-12/schema"
// shared subschemas live under "$defs"
// adds prefixItems (tuples), unevaluatedProperties, items-as-single-schemaUse draft-07 when you must match an existing toolchain; use 2020-12 for new projects that want tuple validation or unevaluatedProperties. Switching draft later means hand-editing the $schema URL and renaming definitions to $defs.
Refine before you ship: enum, format, ranges
The generated schema is a 60-70% starting point. Inference reads structure but never the constraints that make a schema actually protective. A refinement pass closes the gap:
- Add
enumfor closed sets — status, role, country code. A status string inferred as{"type": "string"}lets invalid values pass silently. - Add
formatfor known string shapes:email,date-time,uri,uuid. The tool detects common ones automatically, but verify them. - Add numeric bounds —
minimum,maximum,multipleOffor ports, percentages, quantities. - Add
minLength/maxLengthorpatternfor structured strings like SKUs and slugs. - Trim
requiredto the real contract, and add"type": ["string", "null"]where a field is genuinely nullable.
The tradeoff is fundamental: a single example yields the narrowest schema that accepts it, which is simultaneously too strict (optional fields look required, nulls are rejected) and too loose (no enums, no ranges). For a deeper walkthrough of inference rules and multi-example tools like genson and quicktype, see the guide to generating JSON Schema from JSON. To check your refined schema, paste it into the JSON Schema Validator.