Dev Tools

Real-World JSON Schema Examples: Practical Patterns for APIs, Configs, and Catalogs

Introduction: From Theory to Practice

JSON Schema is a powerful tool for validating data structure, but many developers struggle to move from reading the specification to writing schemas for real applications. The keywords are well-documented — type, properties, required, pattern — but knowing how to combine them into schemas that solve actual business problems is a different skill entirely. This article bridges that gap with four complete, production-ready JSON Schema examples drawn from common software scenarios.

Each example presents a realistic use case, a full schema with annotations, and an explanation of the patterns and keywords involved. You will see how to handle nullable fields, conditional validation with if/then/else, reusable definitions via $ref, enumerations for status fields, and regular expressions for emails, URLs, and semantic version strings. Try each schema in our JSON Schema Validator to see validation in action as you follow along.

Example 1: User Registration API Request

User registration endpoints are among the most common API surfaces in web development. The request body typically includes required identity fields, optional profile data, and fields whose presence depends on account type. A well-designed JSON Schema catches malformed payloads before they reach your business logic, returning clear 422 errors to clients.

The Complete Schema

{
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "UserRegistration",
"description": "Schema for user registration API request body",
"type": "object",
"properties": {
  "email": {
    "type": "string",
    "format": "email",
    "pattern": "^[a-zA-Z0-9._%+\\-]+@[a-zA-Z0-9.\\-]+\\.[a-zA-Z]{2,}$",
    "maxLength": 254,
    "description": "Primary email address for the account"
  },
  "password": {
    "type": "string",
    "minLength": 8,
    "maxLength": 128,
    "description": "Account password, minimum 8 characters"
  },
  "username": {
    "type": "string",
    "pattern": "^[a-zA-Z][a-zA-Z0-9_]{2,29}$",
    "description": "Unique username, 3-30 chars, starts with a letter"
  },
  "fullName": {
    "type": "string",
    "minLength": 1,
    "maxLength": 200
  },
  "accountType": {
    "type": "string",
    "enum": ["personal", "business", "developer"]
  },
  "companyName": {
    "type": ["string", "null"],
    "maxLength": 300,
    "description": "Required for business accounts, nullable for others"
  },
  "taxId": {
    "type": ["string", "null"],
    "pattern": "^[A-Z0-9\\-]{5,20}$",
    "description": "Tax identifier for business accounts"
  },
  "acceptTerms": {
    "type": "boolean",
    "const": true,
    "description": "Must be true to complete registration"
  },
  "marketingOptIn": {
    "type": "boolean",
    "default": false
  }
},
"required": ["email", "password", "username", "accountType", "acceptTerms"],
"if": {
  "properties": {
    "accountType": { "const": "business" }
  },
  "required": ["accountType"]
},
"then": {
  "required": ["companyName", "taxId"]
},
"additionalProperties": false
}

Key Patterns Explained

Email validation with dual enforcement. The schema uses both format: "email" and an explicit pattern regex. The format keyword is an annotation in Draft 7, meaning validators may or may not enforce it. The regex guarantees enforcement regardless of the validator's configuration. The maxLength: 254 aligns with the RFC 5321 limit for email addresses.

Username regex pattern. The pattern ^[a-zA-Z][a-zA-Z0-9_]{2,29}$ enforces that usernames start with a letter and contain only alphanumeric characters and underscores. The quantifier {2,29} on the second group, combined with the required first character, produces a total length between 3 and 30 characters.

Nullable fields with type arrays. The companyName and taxId fields use "type": ["string", "null"] to allow null values. This is the Draft 7 approach for nullable fields — the value can be either a valid string or explicitly null. This is cleaner than omitting the field entirely because it distinguishes between "not provided" (key absent) and "intentionally empty" (key present, value null).

Conditional validation with if/then. When accountType is "business", the if/then block activates, making companyName and taxId required. For personal or developer accounts, these fields remain optional. This pattern avoids complex oneOf constructions and produces clearer validation error messages because the condition is explicit.

Boolean const for terms acceptance. The acceptTerms field uses "const": true, meaning only the value true passes validation. Sending false or omitting the field both produce validation errors, which is exactly the behavior you want for a terms-of-service checkbox in a registration form.

Example 2: E-Commerce Product Catalog Entry

Product catalogs present unique validation challenges: variable attributes per product type, nested pricing structures, inventory tracking, and internationalized content. A JSON Schema for products must be flexible enough to accommodate different categories while strict enough to prevent incomplete listings from reaching the storefront.

The Complete Schema

{
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "ProductCatalogEntry",
"type": "object",
"definitions": {
  "localizedString": {
    "type": "object",
    "properties": {
      "en": { "type": "string", "minLength": 1 },
      "es": { "type": "string", "minLength": 1 }
    },
    "required": ["en"],
    "additionalProperties": { "type": "string" }
  },
  "money": {
    "type": "object",
    "properties": {
      "amount": { "type": "number", "minimum": 0, "multipleOf": 0.01 },
      "currency": { "type": "string", "pattern": "^[A-Z]{3}$" }
    },
    "required": ["amount", "currency"]
  },
  "imageRef": {
    "type": "object",
    "properties": {
      "url": { "type": "string", "format": "uri", "pattern": "^https://" },
      "altText": { "$ref": "#/definitions/localizedString" },
      "width": { "type": "integer", "minimum": 1 },
      "height": { "type": "integer", "minimum": 1 }
    },
    "required": ["url", "altText"]
  }
},
"properties": {
  "sku": {
    "type": "string",
    "pattern": "^[A-Z]{2,4}-\\d{4,8}$",
    "description": "Stock Keeping Unit, e.g. ELEC-00012345"
  },
  "name": { "$ref": "#/definitions/localizedString" },
  "description": { "$ref": "#/definitions/localizedString" },
  "category": {
    "type": "string",
    "enum": ["electronics", "clothing", "home", "books", "sports", "food"]
  },
  "status": {
    "type": "string",
    "enum": ["draft", "active", "discontinued", "out_of_stock"]
  },
  "price": { "$ref": "#/definitions/money" },
  "salePrice": {
    "oneOf": [
      { "$ref": "#/definitions/money" },
      { "type": "null" }
    ]
  },
  "images": {
    "type": "array",
    "items": { "$ref": "#/definitions/imageRef" },
    "minItems": 1,
    "maxItems": 10
  },
  "tags": {
    "type": "array",
    "items": { "type": "string", "minLength": 1, "maxLength": 50 },
    "uniqueItems": true,
    "maxItems": 20
  },
  "weight": {
    "type": "object",
    "properties": {
      "value": { "type": "number", "exclusiveMinimum": 0 },
      "unit": { "type": "string", "enum": ["kg", "lb", "g", "oz"] }
    },
    "required": ["value", "unit"]
  },
  "inventory": {
    "type": "object",
    "properties": {
      "quantity": { "type": "integer", "minimum": 0 },
      "warehouseCode": { "type": "string", "pattern": "^WH-[A-Z]{2,4}-\\d{3}$" },
      "reorderThreshold": { "type": "integer", "minimum": 0 }
    },
    "required": ["quantity", "warehouseCode"]
  }
},
"required": ["sku", "name", "category", "status", "price", "images"]
}

Key Patterns Explained

Reusable definitions with $ref. The definitions block declares three shared sub-schemas: localizedString for internationalized text, money for price amounts with currency codes, and imageRef for product images. Each is referenced with $ref wherever needed, eliminating duplication. If the money format changes — say you add a formattedDisplay field — you update one definition and every reference inherits the change.

Localized strings with flexible languages. The localizedString definition requires English (en) as a baseline but allows additional language keys via additionalProperties: {"type": "string"}. This means you can add French, German, or any other locale without modifying the schema.

Currency precision with multipleOf. The money definition uses "multipleOf": 0.01 to enforce two-decimal precision on amounts. A value like 19.999 fails validation, preventing floating-point rounding issues in financial calculations. The currency code pattern ^[A-Z]{3}$ matches ISO 4217 codes like USD, EUR, and GBP.

Nullable sale price with oneOf. The salePrice field uses oneOf to accept either a valid money object or null. This is an alternative to the type-array approach shown in the registration example, and it works well when the non-null option is a complex type defined via $ref.

Enum for status lifecycle. The status field limits values to a defined set: draft, active, discontinued, and out_of_stock. Enums are ideal for finite state fields because they produce specific error messages — a validator will tell you which values are accepted rather than just reporting a type mismatch.

Array constraints for images and tags. Product images require at least one (minItems: 1) and allow at most ten (maxItems: 10). Tags enforce uniqueItems: true to prevent duplicate entries. These constraints catch data quality issues before products reach the storefront.

Example 3: Application Configuration File

Configuration files are a frequent source of deployment failures. A typo in a database hostname, an invalid port number, or a missing required section can bring down an application at startup. Validating configuration against a JSON Schema during the build or boot process catches these errors before they cause production incidents.

The Complete Schema

{
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "AppConfig",
"description": "Application configuration schema",
"type": "object",
"properties": {
  "appName": { "type": "string", "minLength": 1, "maxLength": 100 },
  "version": {
    "type": "string",
    "pattern": "^\\d+\\.\\d+\\.\\d+(-[a-zA-Z0-9.]+)?$",
    "description": "Semantic version, e.g. 2.1.0 or 2.1.0-beta.1"
  },
  "environment": {
    "type": "string",
    "enum": ["development", "staging", "production", "test"]
  },
  "server": {
    "type": "object",
    "properties": {
      "host": { "type": "string", "format": "hostname" },
      "port": { "type": "integer", "minimum": 1, "maximum": 65535 },
      "basePath": {
        "type": "string",
        "pattern": "^/[a-zA-Z0-9/_-]*$",
        "default": "/"
      },
      "tls": {
        "type": "object",
        "properties": {
          "enabled": { "type": "boolean" },
          "certPath": { "type": "string" },
          "keyPath": { "type": "string" }
        },
        "if": {
          "properties": { "enabled": { "const": true } },
          "required": ["enabled"]
        },
        "then": {
          "required": ["certPath", "keyPath"]
        }
      }
    },
    "required": ["host", "port"]
  },
  "database": {
    "type": "object",
    "properties": {
      "driver": {
        "type": "string",
        "enum": ["postgres", "mysql", "sqlite", "mongodb"]
      },
      "connectionString": {
        "type": "string",
        "pattern": "^(postgres|mysql|mongodb)(ql)?://",
        "description": "Full connection URI"
      },
      "pool": {
        "type": "object",
        "properties": {
          "min": { "type": "integer", "minimum": 0, "default": 2 },
          "max": { "type": "integer", "minimum": 1, "default": 10 },
          "idleTimeoutMs": { "type": "integer", "minimum": 1000, "default": 30000 }
        }
      },
      "ssl": { "type": "boolean", "default": false }
    },
    "required": ["driver", "connectionString"],
    "if": {
      "properties": { "driver": { "const": "sqlite" } },
      "required": ["driver"]
    },
    "then": {
      "properties": {
        "connectionString": { "pattern": "^file:" }
      }
    }
  },
  "logging": {
    "type": "object",
    "properties": {
      "level": {
        "type": "string",
        "enum": ["debug", "info", "warn", "error", "fatal"],
        "default": "info"
      },
      "format": {
        "type": "string",
        "enum": ["json", "text", "pretty"],
        "default": "json"
      },
      "outputPath": {
        "type": ["string", "null"],
        "description": "File path for log output; null for stdout"
      }
    }
  },
  "features": {
    "type": "object",
    "additionalProperties": { "type": "boolean" },
    "description": "Feature flags as key-value boolean pairs"
  }
},
"required": ["appName", "version", "environment", "server", "database"]
}

Key Patterns Explained

Semantic version regex. The version field uses the pattern ^\\d+\\.\\d+\\.\\d+(-[a-zA-Z0-9.]+)?$ to validate semantic versioning strings. It matches 1.0.0, 2.3.1-beta.1, and 0.9.0-rc.2 while rejecting incomplete versions like 1.0 or invalid formats like v1.0.0. The pre-release suffix is optional, captured by the (-[a-zA-Z0-9.]+)? group.

Port range validation. The server port is constrained with "minimum": 1, "maximum": 65535, matching the valid TCP/UDP port range. Combined with "type": "integer", this rejects floating-point values, negative numbers, and port zero (which is reserved).

Nested conditional validation for TLS. The TLS configuration uses a nested if/then pattern: when enabled is true, both certPath and keyPath become required. When TLS is disabled or the tls block is omitted entirely, certificate paths are not required. This prevents the common mistake of enabling TLS without providing certificate files.

Driver-specific connection string validation. The database section uses conditional validation to change the expected connection string format based on the driver. When the driver is sqlite, the connection string must start with file:. For other drivers, the general URI pattern applies. This catches mismatches where a developer configures a PostgreSQL driver but provides a SQLite file path.

Feature flags with additionalProperties. The features object uses "additionalProperties": {"type": "boolean"} instead of explicitly listing each flag. This allows any key name as long as the value is boolean, which is perfect for feature flags that change frequently. New flags can be added without updating the schema, but the schema still ensures no flag accidentally receives a string or number value.

Nullable logging output path. The outputPath uses "type": ["string", "null"] to distinguish between logging to a file (string path) and logging to stdout (null). This avoids the ambiguity of using an empty string, which could be mistaken for a path error.

Example 4: CI/CD Pipeline Configuration

CI/CD pipeline configurations define the steps, environments, and conditions for building, testing, and deploying software. These configurations are critical infrastructure — a validation error can break deployments or, worse, deploy untested code to production. Validating pipeline configs with JSON Schema provides an early safety net.

The Complete Schema

{
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "CICDPipelineConfig",
"type": "object",
"definitions": {
  "environmentVariables": {
    "type": "object",
    "additionalProperties": { "type": "string" },
    "description": "Key-value pairs for environment variables"
  },
  "step": {
    "type": "object",
    "properties": {
      "name": { "type": "string", "minLength": 1, "maxLength": 100 },
      "command": { "type": "string", "minLength": 1 },
      "workingDirectory": { "type": "string", "default": "." },
      "timeout": {
        "type": "integer",
        "minimum": 30,
        "maximum": 7200,
        "default": 600,
        "description": "Step timeout in seconds"
      },
      "continueOnError": { "type": "boolean", "default": false },
      "env": { "$ref": "#/definitions/environmentVariables" },
      "condition": {
        "type": "string",
        "description": "Expression evaluated to determine if step runs"
      }
    },
    "required": ["name", "command"]
  },
  "stage": {
    "type": "object",
    "properties": {
      "name": { "type": "string", "minLength": 1 },
      "dependsOn": {
        "type": "array",
        "items": { "type": "string" },
        "uniqueItems": true,
        "description": "Stages that must complete before this one"
      },
      "environment": {
        "type": "string",
        "enum": ["development", "staging", "production"]
      },
      "approvalRequired": { "type": "boolean", "default": false },
      "steps": {
        "type": "array",
        "items": { "$ref": "#/definitions/step" },
        "minItems": 1
      },
      "env": { "$ref": "#/definitions/environmentVariables" }
    },
    "required": ["name", "steps"],
    "if": {
      "properties": {
        "environment": { "const": "production" }
      },
      "required": ["environment"]
    },
    "then": {
      "properties": {
        "approvalRequired": { "const": true }
      },
      "required": ["approvalRequired"]
    }
  }
},
"properties": {
  "pipelineName": {
    "type": "string",
    "pattern": "^[a-zA-Z][a-zA-Z0-9_-]{1,63}$"
  },
  "version": {
    "type": "string",
    "pattern": "^\\d+\\.\\d+$",
    "description": "Pipeline config version, e.g. 1.0"
  },
  "trigger": {
    "type": "object",
    "properties": {
      "branches": {
        "type": "array",
        "items": { "type": "string", "minLength": 1 },
        "minItems": 1
      },
      "events": {
        "type": "array",
        "items": {
          "type": "string",
          "enum": ["push", "pull_request", "tag", "schedule", "manual"]
        },
        "minItems": 1,
        "uniqueItems": true
      },
      "paths": {
        "type": "array",
        "items": { "type": "string" },
        "description": "File path filters that trigger the pipeline"
      },
      "schedule": {
        "type": "string",
        "pattern": "^(@(annually|yearly|monthly|weekly|daily|hourly)|(((\\d+|\\*)(,\\d+)*(/\\d+)?\\s*){5}))$",
        "description": "Cron expression or predefined schedule"
      }
    },
    "required": ["events"]
  },
  "globalEnv": { "$ref": "#/definitions/environmentVariables" },
  "stages": {
    "type": "array",
    "items": { "$ref": "#/definitions/stage" },
    "minItems": 1,
    "maxItems": 20
  },
  "notifications": {
    "type": "object",
    "properties": {
      "onSuccess": {
        "type": "array",
        "items": {
          "type": "string",
          "format": "email",
          "pattern": "^[a-zA-Z0-9._%+\\-]+@[a-zA-Z0-9.\\-]+\\.[a-zA-Z]{2,}$"
        }
      },
      "onFailure": {
        "type": "array",
        "items": {
          "type": "string",
          "format": "email",
          "pattern": "^[a-zA-Z0-9._%+\\-]+@[a-zA-Z0-9.\\-]+\\.[a-zA-Z]{2,}$"
        }
      },
      "webhookUrl": {
        "type": ["string", "null"],
        "format": "uri",
        "pattern": "^https://"
      }
    }
  }
},
"required": ["pipelineName", "version", "trigger", "stages"]
}

Key Patterns Explained

Multi-level $ref reuse. The pipeline schema defines three reusable components: environmentVariables, step, and stage. The stage definition itself references both step and environmentVariables, demonstrating how $ref can compose definitions across multiple levels. This hierarchical approach keeps each definition focused on a single concern.

Production deployment safety gate. The stage definition uses conditional validation to enforce that production deployments require manual approval. When environment is "production", the if/then block requires approvalRequired to be true. This is a schema-level safeguard against accidental production deployments, complementing whatever approval mechanisms exist in the CI/CD platform itself.

Event types as enums. Trigger events are constrained to a known set: push, pull_request, tag, schedule, and manual. Combined with uniqueItems: true, the schema prevents duplicate event entries and rejects unknown event types. Adding a new event type later only requires updating the enum array.

Cron expression pattern. The schedule field accepts either a predefined shorthand (@daily, @weekly, etc.) or a standard five-field cron expression. The regex validates the general structure without fully parsing cron semantics — a pragmatic trade-off between validation strictness and regex complexity. For full cron validation, application-level logic is more appropriate.

Webhook URL requiring HTTPS. The webhookUrl uses both format: "uri" and pattern: "^https://" to ensure notification webhooks use encrypted connections. The field is nullable, allowing configurations that do not use webhook notifications to explicitly set the value to null rather than omitting the key.

Step timeout boundaries. Each step's timeout is bounded between 30 seconds and 7,200 seconds (two hours) with a default of 600 seconds (ten minutes). The lower bound prevents accidentally setting a timeout so low that normal operations fail, while the upper bound prevents runaway steps from consuming CI/CD minutes indefinitely.

Cross-Cutting Patterns and Best Practices

Several patterns appear across all four examples. Understanding these recurring techniques will help you apply them to your own schemas regardless of the domain.

Choosing Between Nullable Types and Optional Properties

Use optional properties (omitting them from required) when a field's absence is normal and carries no semantic meaning. Use nullable types ("type": ["string", "null"]) when you need to distinguish between "not provided" and "intentionally empty." In API responses, nullable fields communicate that the server acknowledged the field but had no value to return — a subtle but important distinction for client-side logic.

When to Use if/then/else vs. oneOf

Conditional validation with if/then/else is best when a single discriminator field determines additional requirements. It produces clear, targeted error messages. Use oneOf when the valid shapes are fundamentally different — for example, a payment method that is either a credit card object or a bank transfer object with entirely different fields. Avoid oneOf for simple conditional requirements because it generates confusing errors listing all unmatched alternatives.

Structuring Definitions for Maximum Reuse

Place shared sub-schemas in the definitions block (or $defs in later drafts). Good candidates for definitions include: value objects that appear in multiple places (money, localized strings, addresses), entities with their own validation rules (users, products), and constraint patterns you apply to multiple fields (non-empty strings, positive integers). Name definitions using PascalCase for complex types and camelCase for simple patterns. Reference them with $ref: "#/definitions/TypeName" throughout the schema.

Regex Patterns for Common Formats

Several regex patterns recur across real-world schemas. Here is a reference table of the patterns demonstrated in this article:

  • Email: ^[a-zA-Z0-9._%+\\-]+@[a-zA-Z0-9.\\-]+\\.[a-zA-Z]{2,}$ — matches standard email addresses. Not fully RFC 5322 compliant but covers the vast majority of real-world addresses.
  • Semantic version: ^\\d+\\.\\d+\\.\\d+(-[a-zA-Z0-9.]+)?$ — matches versions like 1.0.0 and 2.3.1-beta.1.
  • HTTPS URL prefix: ^https:// — a simple pattern to enforce secure connections. Combine with format: "uri" for full URI validation.
  • ISO 4217 currency code: ^[A-Z]{3}$ — matches three uppercase letters like USD, EUR, GBP.
  • SKU/identifier format: ^[A-Z]{2,4}-\\d{4,8}$ — matches structured identifiers with a letter prefix and numeric suffix.

Default Values and Documentation

Use the default keyword to document expected behavior when a field is omitted. While JSON Schema validators do not insert defaults into the data (that is the application's responsibility), defaults serve as machine-readable documentation. Similarly, use the description keyword liberally — these descriptions can be extracted to generate API documentation, configuration guides, and IDE auto-completion hints.

Conclusion

JSON Schema is most valuable when applied to real data with real constraints. The four examples in this article — user registration, product catalogs, application configuration, and CI/CD pipelines — demonstrate how core JSON Schema features combine to solve practical validation problems. Nullable types model intentional absence. Conditional validation enforces context-dependent rules. Reusable definitions keep schemas maintainable as they grow. Enums lock down finite state fields. And regex patterns validate format-sensitive strings.

Start with the example closest to your use case, adapt it to your specific fields and rules, and validate it interactively using our JSON Schema Validator. A schema that validates your real data today will save you from debugging malformed data in production tomorrow.

social: openGraph: type: "article"
← Back to Blog