Back to Blog
Developer Guides7 min readJuly 11, 2026The Toolbox Team

How to Generate a JSON Schema from a Sample Payload

Generate a JSON Schema from an existing JSON payload in seconds, then tighten it by hand. Covers types, required fields, enums, formats, and the four things a generator can't infer.

Generate the skeleton, hand-write the constraints

If you have ever needed a JSON Schema — to validate an API contract, lock down a config file, or constrain an LLM's structured output — you have probably done it the slow way: the spec open in one tab, your payload in another, typing the whole thing out by hand. It is tedious, and it is easy to quietly get required wrong.

You do not have to start from a blank file. A schema can be inferred from a representative payload, which gets you the entire structure in one step. You then tighten it by hand. That turns a twenty-minute job into a two-minute one, and the part you do by hand is the part that actually needs judgement.

This guide walks through both halves: generating the skeleton, and the four things a generator fundamentally cannot know.

Step 1: Start with a representative payload

The quality of the generated schema depends entirely on how representative your sample is. Use a real response with every field populated, not a minimal example.

{
  "id": 4021,
  "email": "ada@example.com",
  "active": true,
  "roles": ["admin", "billing"],
  "profile": {
    "displayName": "Ada L.",
    "avatarUrl": null
  }
}

Step 2: Generate the schema

Paste that into the JSON Schema Generator. It works out the types, the nesting, the array item types, and which fields are required:

{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "type": "object",
  "properties": {
    "id":     { "type": "integer" },
    "email":  { "type": "string" },
    "active": { "type": "boolean" },
    "roles":  { "type": "array", "items": { "type": "string" } },
    "profile": {
      "type": "object",
      "properties": {
        "displayName": { "type": "string" },
        "avatarUrl":   { "type": ["string", "null"] }
      },
      "required": ["displayName", "avatarUrl"]
    }
  },
  "required": ["id", "email", "active", "roles", "profile"]
}

A few things worth noticing in that output. id came out as integer, not number — JSON Schema distinguishes the two, and a generator that gets this right saves you a subtle bug. avatarUrl was null in the sample, so it became a union type ["string", "null"] rather than being dropped. And the nested profile object got its own required array, independent of the parent's.

The tool supports draft-04, draft-07, 2019-09, and 2020-12. If you are not sure which to pick, 2020-12 is the current standard; choose draft-07 only if a library you depend on has not caught up. Everything runs in your browser — the payload is never uploaded, which matters when you are pasting a real production response.

Step 3: Fix the four things a generator cannot know

Inference gives you the shape. It cannot read your intent. These four always need a human:

1. Formats

To a generator, an email address is just a string:

"email": { "type": "string" }

Nothing in the payload says it must be an email. Add the constraint yourself:

"email": { "type": "string", "format": "email" }

The commonly supported values are email, uri, uuid, date, date-time, ipv4, and hostname. Note that in JSON Schema, format is an annotation by default — some validators only enforce it if you turn on format assertion. Check your validator's behaviour rather than assuming.

2. Enums

roles was inferred as an array of arbitrary strings:

"roles": { "type": "array", "items": { "type": "string" } }

If the only legal values are admin, billing, and viewer, then as written the schema validates nothing useful — "roles": ["banana"] passes. Say what you mean:

"roles": {
  "type": "array",
  "items": { "enum": ["admin", "billing", "viewer"] }
}

3. required is a guess

This is the one that bites people. The generator marks a field required because it happened to be present in your sample — not because it is genuinely mandatory. An optional field that was populated in your test payload gets promoted to required, and now valid requests fail validation in production.

Read every entry in every required array and ask: is this field truly always present? In the example above, avatarUrl is almost certainly optional, yet it was marked required inside profile.

4. additionalProperties

By default, JSON Schema lets an object carry extra properties beyond those you declared. If your object is a closed contract — say, a webhook body you want to reject if it drifts — you must say so explicitly:

"additionalProperties": false

Leave it permissive for anything you want to remain forward-compatible, and close it for anything you want to police strictly. The generator cannot make that call for you.

When inference is the wrong approach

Generating from a sample is the right move when you already have data and need a schema to describe it — documenting an existing API, validating configs, or pinning down an LLM's output shape.

It is the wrong move when the schema is the contract you are designing up front. If you are specifying an API before it exists, write the schema first and generate example payloads from it, not the other way round. Inferring a contract from one sample response bakes that sample's accidents into your spec.

A quick sanity check

Once you have tightened the schema, validate the original payload against it. It should pass. Then deliberately break it — remove a required field, put a bad value in an enum — and confirm it fails. A schema that has never rejected anything is a schema you have not actually tested.

If your JSON is messy to begin with, run it through the JSON Formatter first so the structure is easy to read before you generate from it.

Summary

  • Generate the skeleton from a representative payload; do not hand-write it.
  • Prefer draft 2020-12 unless a dependency forces otherwise.
  • Then add by hand: formats, enums, a corrected required list, and an explicit additionalProperties.
  • Test the schema by making it fail, not just by making it pass.

Generate the skeleton, hand-write the constraints — that split is the whole technique.