quicktype JSON Schema: Infer Schemas from JSON Samples
Abhay khant
Jan 1, 1970 • 10 min read
quicktype turns a pile of JSON samples into a working JSON Schema in one command: quicktype --lang schema --input sample.json --output inferred.schema.json. Point it at real payloads from a legacy API, a third-party webhook, or a log dump, and it reverse-engineers the shape of your data as a machine-readable contract (1). You get a starting schema in seconds, then refine it for the constraints inference cannot guess.
JSON Schema is the vocabulary that describes what valid JSON looks like, and Draft 2020-12 is the current stable specification (3). Inferred schemas are not a replacement for carefully designed contracts; they are a fast way to bootstrap one when no types exist. This guide covers when data-first inference makes sense, how to feed quicktype well, what its output gets right and wrong, and how to harden the result before it reaches validation and CI.
When data-first inference makes sense
Code-first generation, where a library like Zod exports JSON Schema from TypeScript types, is the recommended path for new projects (6). But three situations have no types to start from:
- Legacy APIs with undocumented or outdated docs
- Third-party webhooks whose payloads arrive unannounced
- Log and analytics data with shapes that evolved organically
In all three cases the JSON exists and the schema does not. Data-first inference closes that gap by reading the payloads themselves (1). The tools directory lists inference tools alongside code-first generators and schema-to-code converters, so the approach is a recognized category, not a hack (4).
Installing quicktype
quicktype runs anywhere Node.js does. Install it globally:
npm install -g quicktype
Verify the install:
quicktype --version
The CLI accepts JSON from a file, a directory of files, a URL, or stdin (2):
quicktype --lang schema --input user.json --output user.schema.json
cat user.json | quicktype --lang schema -o user.schema.json
quicktype --lang schema --input https://api.example.com/users/1 -o user.schema.json
For a first pass, a single well-formed sample is enough. The real gains come from feeding many samples, which is the next section.
A worked example
Suppose a legacy API returns user records that look like this:
{
"id": 42,
"name": "Ava Torres",
"email": "ava@example.com",
"role": "admin",
"active": true
}
Running inference on a directory of such records produces a schema:
quicktype --lang schema --input samples/ -o user.schema.json
The generated Draft 7 schema mirrors the structure you saw:
{
"$schema": "http://json-schema.org/draft-07/schema#",
"type": "object",
"properties": {
"id": { "type": "integer" },
"name": { "type": "string" },
"email": { "type": "string" },
"role": { "type": "string" },
"active": { "type": "boolean" }
},
"required": ["id", "name", "email", "role", "active"]
}
Note what is already right: the object shape, the property types, and the required list, because every sample contained every field. Note what is missing: email has no format keyword and role has no enum, because inference has no semantic understanding of what those strings mean. The next two sections cover exactly those gaps.
Feed multiple samples, not one
Inference generalizes from what it sees. A single sample tells quicktype one example of each field; ten samples show which fields vary, which disappear, and which values repeat (1). Run it over several files at once:
quicktype --lang schema \
--input sample-1.json sample-2.json sample-3.json \
--output user.schema.json
The more distinct real payloads you provide, the closer the inferred schema gets to the actual contract. If a field is absent from one sample, inference marks it optional. If a field is a number in one sample and a string in another, inference produces a union. Collect payloads that cover edge cases: empty arrays, missing optional fields, nulls, and the most extreme values you have seen.
Format and inspect your samples before inference so malformed payloads do not poison the result (ToolSura JSON formatter). A stray "1,000" string in one record can push an integer field into a string-union that propagates through the whole schema.
A common real-world flow is wiring quicktype into a webhook intake. You collect a week of raw events, deduplicate them, and infer from the whole batch. The resulting schema then becomes the documented contract for the webhook, and new events are validated against it in staging before you trust them in production. The batch size directly determines how many optional branches the schema captures, so capture more rather than fewer samples.
Options that change the output
quicktype's flags matter as much as the samples:
--top-level <name>names the root type instead of the default--just-typesemits types only, dropping serializer code where the target language has any--acronym-stylecontrols how names likeapiKeybecomeApiKeyorAPIKey--runtime-typecheckadds runtime validation helpers for TypeScript output--packageand--moduleadjust generated import structure for languages with modules
These are cosmetic until they are not. A schema named Root instead of User ripples into every generated client, so set --top-level before you commit the file.
The same options also keep output deterministic. If your CI re-runs inference to detect drift, a schema whose naming style or top-level name shifts between runs produces a noisy diff that hides the real changes. Pin the options in a small script, commit it next to the samples, and run inference from that script everywhere. Run quicktype --help to see the full list for your target language (1).
What quicktype gets right
For plain JSON structures, the inference is solid:
- Objects become
type: "object"withproperties - Arrays become
type: "array"withitems - Strings, numbers, booleans, and nulls map to their JSON Schema types
- Fields present in every sample land in
required - Repeated string values across samples can surface as
enum
quicktype emits Draft 7 by default (1). That matters when you feed the schema to modern tooling, because Draft 2020-12 moved definitions to $defs and changed how format behaves (3). Most validators still read Draft 7, but if your pipeline expects the newer draft, budget for a migration step. The JSON Schema generator guide includes a checklist for moving Draft 7 schemas to Draft 2020-12.
What inference cannot know
Inference sees JSON types, not semantics. The gaps are consistent and worth memorizing:
- Formats: inference is pattern-based and has no semantic understanding. A string holding an email is just a string, so
format: "email"will not appear in the output unless you add it (1). Pattern detection may flag obvious dates or UUIDs in some targets, but you cannot rely on it, and plain-string fields like names and emails never gain formats. Addformatkeywords by hand or enforce them with a validation layer. - Required fields: a field that happens to be missing from every sample is inferred as optional, even if the API always sends it.
- Enums: a role field with one value in your samples produces a plain string, not an enum. Inference needs multiple distinct values to generalize.
- Numeric ranges: nothing suggests that
ageshould be non-negative or bounded, because the JSON contains no such signal.
This is why inferred schemas are starting points, not finished contracts. With code-first generation you get constraints from types; with samples you get structure. The tradeoff is covered from the TypeScript side in our Zod to JSON Schema guide.
Refine the output
After generation, tighten the schema in two passes. First, add the semantics inference missed:
{
"type": "object",
"properties": {
"id": { "type": "integer", "minimum": 1 },
"email": { "type": "string", "format": "email" },
"createdAt": { "type": "string", "format": "date-time" },
"role": { "type": "string", "enum": ["admin", "user", "viewer"] },
"age": { "type": "integer", "minimum": 0, "maximum": 150 }
},
"required": ["id", "email", "createdAt"]
}
Second, validate the result against real payloads before wiring it into a pipeline. The ToolSura JSON validator accepts a schema and a sample payload side by side, so you can catch over-strict or over-loose constraints early.
If your stack is TypeScript, an alternative to hand-refining is layering Zod on top: keep the inferred schema for documentation and interop, but define the real runtime contract in Zod so formats, ranges, and enums are enforced in code (6). The inferred schema stays a faithful structural sketch; the Zod schema carries the semantic truth.
From schema back to code
The inferred schema is not the end of the line. quicktype reads JSON Schema as an input format too, and generates typed models and serializers in roughly 25 languages, including TypeScript, Go, Python, Rust, Java, and C# (1, 2):
quicktype user.schema.json -o user.ts
quicktype user.schema.json -o user.go
quicktype user.schema.json -o user.py
This closes the loop for a polyglot team: infer once from samples, refine the schema, then generate per-language types from that single source of truth. Dedicated converters like json-schema-to-typescript give finer control over the TypeScript output when you outgrow quicktype's defaults (8). The same schemas also drop into OpenAPI 3.1 documents, which use JSON Schema as their Schema Object format, so your inferred contract can feed API documentation with no conversion step (7).
Validation and CI integration
Once the schema is committed, enforce it. AJV compiles JSON Schema into fast JavaScript validation functions and is the de facto standard validator (5):
import Ajv from "ajv";
const ajv = new Ajv({ strict: true });
const validate = ajv.compile(userSchema);
if (!validate(req.body)) {
return res.status(400).json({ errors: validate.errors });
}
In CI, add a step that compiles the schema and validates representative fixtures, so drift fails the build:
name: Schema Validation
on: [push, pull_request]
jobs:
validate:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: npm ci
- run: npx ajv compile -s schemas/*.json
- run: npx ajv validate -s schemas/user.schema.json -d test/fixtures/valid-user.json
Keep known-good and known-bad fixtures committed. When the API shape changes, updating the fixtures and re-running inference makes the schema change visible in the pull request (5).
quicktype vs code-first generation
Choose by what you have, not by preference:
- No types, only JSON (legacy APIs, webhooks, logs): start with quicktype inference, then refine
- TypeScript codebase: generate JSON Schema from Zod or Valibot instead; the schema stays a derived artifact and cannot drift from the types (6)
- Existing JSON Schema files: skip inference entirely and use schema-to-code converters for the other languages (8)
The four approaches, including manual authoring for one-off contracts and model-first generation from OpenAPI or Protobuf, are compared in full in the JSON Schema generator guide.
Related tools and further reading
- Compare all four generation approaches in our JSON Schema generator guide
- Generate schemas from TypeScript types instead with our Zod to JSON Schema guide
- Format and inspect samples before inference with the ToolSura JSON formatter
- Validate a refined schema against real payloads with the ToolSura JSON validator
References
[1] quicktype — https://github.com/quicktype/quicktype [2] quicktype.io — https://quicktype.io/ [3] JSON Schema Specification — https://json-schema.org/specification.html [4] JSON Schema Tools Directory — https://json-schema.org/tools.html [5] AJV Validator Documentation — https://ajv.js.org/ [6] Zod JSON Schema Conversion — https://zod.dev/json-schema [7] OpenAPI 3.1 Specification — https://spec.openapis.org/oas/v3.1.0.html [8] json-schema-to-typescript — https://github.com/bcherny/json-schema-to-typescript


