← Back to Blog

JSON Schema in Practice: Validating Data and Generating Types

Starting from an annoying production incident

The most common trap in front/back-end integration: the backend assumes price is a number, the frontend sends "128.5" (a string) or omits userId, and it only blows up with a 500 at runtime. Human review cannot catch this class of bug, and intuition won't cover all cases—the right move is to use JSON Schema to declare up front, as a contract, "this must be a number, this is required, the array needs at least one element".

This article walks you from validation to type generation.

A minimal schema

{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "type": "object",
  "required": ["id", "total"],
  "properties": {
    "id":   { "type": "string", "minLength": 1 },
    "total": { "type": "number", "minimum": 0 },
    "items": { "type": "array", "minItems": 1,
               "items": { "type": "object",
                          "properties": {
                            "sku": { "type": "string" },
                            "price": { "type": "number", "minimum": 0 }
                          }, "required": ["sku", "price"] } },
    "status": { "enum": ["paid", "pending", "cancelled"] }
  }
}

This schema expresses four hard rules: id/total required, total ≥ 0, items a non-empty array whose elements all contain sku+price, and status limited to one of three values. Run a validator against it and problems surface immediately.

Core keyword cheat sheet

Keyword Purpose
type object/array/string/number/integer/boolean/null
required required field list
properties per-field sub-schemas of an object
items / prefixItems array element schema
enum allowed value set
const fixed value (discriminator)
allOf/anyOf/oneOf combine multiple sub-schemas
$ref reference an already-defined schema (reuse)
format / pattern format and regex constraints

Tightening the generated schema with strictness

Auto-generating from a JSON sample (json-to-jsonschema) yields loose inference—a fine starting point but with no enforcing power. To make it actually validate, add three things by hand:

  1. Explicit required: list every field that must be present;
  2. Value constraints: add enum/format (date-time, email) to strings, minimum/maximum to numbers, minItems/maxItems to arrays;
  3. A single entry via $ref + $defs: extract reusable objects into $defs and reference them with $ref, so the schema doesn't bloat with duplication.

oneOf for discriminated types

Integration often hits event payloads whose shape differs by a field value—like a notification where type tells email from sms:

"oneOf": [
  { "properties": { "type": { "const": "email" }, "to": { "type": "string" } }, "required": ["to"] },
  { "properties": { "type": { "const": "sms" },   "phone": { "type": "string" } }, "required": ["phone"] }
]

oneOf matches exactly one; the validator can use the const discriminator to match precisely.

Generating strong types from a schema

Once the schema is the single source of truth, the rest is "translate it into each language's types":

JSON sample ──json-to-jsonschema──▶ Schema ──schema-to-typescript──▶ interface definitions
                                          ──schema-to-go──▶       struct definitions
  • JSON → type: fast, but detached from structural constraints; a sample missing a field means a missing type;
  • Schema → type: preserves enum/required/nullable semantics, produces more reliable output, and validation and types stay in sync.

Wire the whole chain into your JSON tooling: format → generate schema → tighten → generate DTOs in one run.

Landing checklist

  • [ ] Define a schema for every API endpoint, saved as a versioned file;
  • [ ] Start with json-to-jsonschema, then add required/constraints by hand;
  • [ ] Drive runtime validation (backend or API gateway) and type generation from the same source;
  • [ ] Use oneOf + const for discriminated unions and $ref to reuse common objects;
  • [ ] Once the contract is set, generate TypeScript/Go DTOs instead of hand-writing them.

Self-check

Take a real API response from a service you own and run the full flow: format → generate schema → fill in required → generate TS types. Confirm the resulting types let your compiler catch a "missing field" risk directly. When that step works, you have truly grasped the value of JSON Schema.

Frequently Asked Questions

What is JSON Schema mainly used for?

It is a standard that describes what JSON data must look like: a node must be a number, an array must have at least one element, a field is required, a value must come from an enum, etc. Typical uses: runtime validation of API request/response, front-end form validation, documenting data contracts, and driving strongly-typed generation (TypeScript/Go types). In essence it lifts 'structure plus constraints' out of code into a reusable, cross-language declaration.

How do I turn a JSON sample into a validating schema?

The fastest path is a 'JSON to Schema' generator (usually built into JSON tool sites, e.g. json-to-jsonschema), which infers types, arrays and nesting from the sample. After generation, tighten it by hand: add enums or format constraints (date-time/email), an explicit required list, minItems on arrays and minimum on numbers—turning loose inference into a strict contract that validation can actually enforce.

When to use oneOf, anyOf and allOf?

They combine sub-schemas differently: **allOf** requires every sub-schema to hold (stacking constraints—commonly adding extensions to a base object); **anyOf** accepts any one (loose polymorphism, e.g. 'string or null'); **oneOf** requires exactly one (strict polymorphism—good for discriminated unions, like distinguishing event payloads by activity.type). For discriminated unions prefer oneOf plus a const discriminator field in each sub-schema so the validator can match precisely.

What is the difference between generating from JSON and from Schema?

The starting points differ: **JSON → type** infers from an instance—fast but only reflects one sample and misses absent fields and constraints; **Schema → type** (e.g. json-schema-to-typescript) derives from a contract and preserves enum, required and nullable semantics, yielding cleaner, documented types. In production, treat the Schema as the single source of truth that drives both runtime validation and type generation, avoiding dual maintenance of 'API doc' and DTOs.

← Back to Blog