DEV Community

varun9358
varun9358

Posted on Originally published at getthetoolbox.com

Stop hand-writing JSON Schema — generate it from a sample payload

Generate the skeleton, hand-write the constraints

If you've ever needed a JSON Schema — to validate an API contract, lock down a config file, or constrain an LLM's structured output — you've probably done it the slow way: spec open in one tab, payload in another, typing the whole thing out by hand.

A better approach: infer the schema from a representative payload, 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.

Step 1: Start with a representative payload

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
  }
}
Enter fullscreen mode Exit fullscreen mode

Step 2: Generate the schema

Paste that into the JSON Schema Generator. It works out the types, the nesting, the array item types, common string formats, and which fields look required. With 2020-12 selected and "Generate descriptions" switched off, you get:

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

A few things worth noticing: id came out as integer, not number — getting that distinction right saves a subtle bug later. email picked up "format": "email" because the sample value looks like one. avatarUrl was null in the sample, and a null says nothing about what the field holds the rest of the time, so the generator can only write { "type": "null" } and leave it out of required. And the nested profile object got its own independent required array.

The tool supports draft-04 through 2020-12, runs entirely client-side, and never uploads your payload — which matters when you're pasting real production data.

Step 3: Fix the four things a generator can't know

Inference gives you the shape. It can't read your intent. Four things always need a human pass:

1. Formats — a detected format is a guess from one value. email above is right, but a field that held https://example.com gets uri even if relative paths are allowed, and an empty string gets no format at all. Check each one and add what the sample couldn't show. Commonly supported values: email, uri, uuid, date, date-time, ipv4, hostname.

2. Enums — roles was inferred as an array of arbitrary strings. If the only legal values are admin, billing, and viewer, say so explicitly with an enum, or the schema won't actually validate anything meaningful.

3. required is a guess — the generator marks a field required because it happened to be present in your one sample, not because it's genuinely mandatory. Read every entry and ask whether it's truly always present. And give nullable fields their real type: avatarUrl should be ["string", "null"] (with "format": "uri"), not "null".

4. additionalProperties — JSON Schema lets objects carry extra properties by default, but this generator writes "additionalProperties": false on every object, so the schema starts closed. That's right for a closed contract (say, a webhook body you want to reject if it drifts) and wrong for anything that should stay forward-compatible: one new field from the API and every response fails. Decide per object.

A quick sanity check

Validate your original payload against the tightened schema — it should pass. Then deliberately break it (drop a required field, put a bad value in an enum) and confirm it fails. A schema that's never rejected anything hasn't really been tested.

Related converters

A few other free, browser-only tools that come up in the same workflow:

Generate the skeleton from a representative payload, then hand-tighten formats, enums, required, and additionalProperties. That split is the whole technique.

Originally published on The Toolbox blog.

Top comments (0)