mcpField
W
writing-json-schemas

writing-json-schemas

github.com/zagginoDevelopment349272 installsUpdated 5d ago
Repository

About writing-json-schemas

writing-json-schemas is a skill for authoring idiomatic JSON Schemas intended for validation with z-schema. It covers schema structure, constraints, composition, references and draft-specific practices, with draft-2020-12 as the default target.

Key features

  1. 01Draft-2020-12 schema templates
  2. 02Object and array constraints
  3. 03Combinators and conditional schemas
  4. 04Reusable definitions and references
  5. 05Guidance on strict properties

Use cases

  1. 01Defining schemas for application data
  2. 02Composing schemas with allOf and $defs
  3. 03Choosing tuple syntax for draft-2020-12

Install

Copy the command and run it in a terminal.

$npx skills add https://github.com/zaggino/z-schema --skill writing-json-schemas

After installing, type/writing-json-schemasin Claude Code to confirm it loaded.

Details

From the project README

Writing JSON Schemas for z-schema

Write correct, idiomatic JSON Schemas validated by z-schema. Default target: draft-2020-12.

Schema template

Start every schema with a $schema declaration and type:

{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "type": "object",
  "properties": {},
  "required": [],
  "additionalProperties": false
}

Set additionalProperties: false explicitly when extra properties should be rejected — z-schema allows them by default.

Object schemas

Basic object with required fields
{
  "type": "object",
  "properties": {
    "name": { "type": "string", "minLength": 1 },
    "email": { "type": "string", "format": "email" },
    "age": { "type": "integer", "minimum": 0, "maximum": 150 }
  },
  "required": ["name", "email"],
  "additionalProperties": false
}
Nested objects
{
  "type": "object",
  "properties": {
    "address": {
      "type": "object",
      "properties": {
        "street": { "type": "string" },
        "city": { "type": "string" },
        "zip": { "type": "string", "pattern": "^\\d{5}(-\\d{4})?$" }
      },
      "required": ["street", "city"]
    }
  }
}
Dynamic property names

Use patternProperties to validate property keys by regex:

{
  "type": "object",
  "patternProperties": {
    "^x-": { "type": "string" }
  },
  "additionalProperties": false
}

Use propertyNames (draft-06+) to constrain all property key strings:

{
  "type": "object",
  "propertyNames": { "pattern": "^[a-z_]+$" }
}

Array schemas

Uniform array
{
  "type": "array",
  "items": { "type": "string" },
  "minItems": 1,
  "uniqueItems": true
}
Tuple validation (draft-2020-12)

Use prefixItems for positional types, items for remaining elements:

{
  "type": "array",
  "prefixItems": [{ "type": "string" }, { "type": "integer" }],
  "items": false
}

items: false rejects extra elements beyond the tuple positions.

Contains (draft-06+)

Require at least one matching item:

{
  "type": "array",
  "contains": { "type": "string", "const": "admin" }
}

With count constraints (draft-2019-09+):

{
  "type": "array",
  "contains": { "type": "integer", "minimum": 10 },
  "minContains": 2,
  "maxContains": 5
}

String constraints

{
  "type": "string",
  "minLength": 1,
  "maxLength": 255,
  "pattern": "^[A-Za-z0-9_]+$"
}
Format validation

z-schema has built-in format validators: date, date-time, time, email, idn-email, hostname, idn-hostname, ipv4, ipv6, uri, uri-reference, uri-template, iri, iri-reference, json-pointer, relative-json-pointer, regex, duration, uuid.

{ "type": "string", "format": "date-time" }

Format assertions are always enforced by default (formatAssertions: null). For vocabulary-aware behavior in draft-2020-12, set formatAssertions: true on the validator.

Numeric constraints

{
  "type": "number",
  "minimum": 0,
  "maximum": 100,
  "multipleOf": 0.01
}

Use exclusiveMinimum / exclusiveMaximum for strict bounds:

{ "type": "integer", "exclusiveMinimum": 0, "exclusiveMaximum": 100 }

Combinators

anyOf — match at least one
{
  "anyOf": [{ "type": "string" }, { "type": "number" }]
}
oneOf — match exactly one
{
  "oneOf": [
    { "type": "string", "maxLength": 5 },
    { "type": "string", "minLength": 10 }
  ]
}
allOf — match all

Use for schema composition. Combine base schemas with refinements:

{
  "allOf": [{ "$ref": "#/$defs/base" }, { "properties": { "extra": { "type": "string" } } }]
}
not — must not match
{ "not": { "type": "null" } }
if / then / else (draft-07+)

Conditional validation — prefer over complex oneOf when the logic is "if X then require Y":

{
  "type": "object",
  "properties": {
    "type": { "type": "string", "enum": ["personal", "business"] },
    "company": { "type": "string" }
  },
  "if": { "properties": { "type": { "const": "business" } } },
  "then": { "required": ["company"] },
  "else": {}
}
When to use which combinator
ScenarioUse
Value can be multiple typesanyOf
Exactly one variant must matchoneOf
Compose inherited schemasallOf
"if condition then require fields"if/then/else
Exclude a specific shapenot

Prefer if/then/else over oneOf when the condition is a single discriminator field — it produces clearer error messages.

Schema reuse with $ref and $defs

Local definitions
{
  "$defs": {
    "address": {
      "type": "object",
      "properties": {
        "street": { "type": "string" },
        "city": { "type": "string" }
      },
      "required": ["street", "city"]
    }
  },
  "type": "object",
  "properties": {
    "home": { "$ref": "#/$defs/address" },
    "work": { "$ref": "#/$defs/address" }
  }
}
Cross-schema references

Compile an array of schemas and reference by ID:

import ZSchema from 'z-schema';

const schemas = [
  {
    $id: 'address',
    type: 'object',
    properties: { city: { type: 'string' } },
    required: ['city'],
  },
  {
    $id: 'person',
    type: 'object',
    properties: {
      name: { type: 'string' },
      home: { $ref: 'address' },
    },
    required: ['name'],
  },
];

const validator = ZSchema.create();
validator.validateSchema(schemas);
validator.validate({ name: 'Alice', home: { city: 'Paris' } }, 'person');

Strict schemas with unevaluatedProperties (draft-2019-09+)

When combining schemas with allOf, additionalProperties: false in a sub-schema blocks properties defined in sibling schemas. Use unevaluatedProperties instead — it tracks all properties evaluated across applicators:

{
  "allOf": [
    {
      "type": "object",
      "properties": { "name": { "type": "string" } },
      "required": ["name"]
    },
    {
      "type": "object",
      "properties": { "age": { "type": "integer" } }
    }
  ],
  "unevaluatedProperties": false
}

This accepts { "name": "Alice", "age": 30 } but rejects { "name": "Alice", "age": 30, "extra": true }.

Validating the schema itself

Always validate schemas at startup:

const validator = ZSchema.create();
try {
  validator.validateSchema(schema);
} catch (err) {
  console.log('Schema errors:', err.details);
}

Common mistakes

  • Forgetting additionalProperties: By default, extra properties are allowed. Set additionalProperties: false or use unevaluatedProperties: false to reject them.
  • Using additionalProperties: false with allOf: This blocks properties from sibling schemas. Use unevaluatedProperties: false at the top level instead (draft-2019-09+).
  • Array items in draft-2020-12: Use prefixItems for tuple validation. items now means "schema for remaining items".
  • Missing $schema: Without it, z-schema uses its configured default draft. Include $schema for explicit draft targeting.
  • definitions vs $defs: Both work, but $defs is the canonical form in draft-2019-09+. Use it consistently.

Comments

Markdown supported