DevTools Hub

Search tools

Search for a developer tool

JSON Schema for Blog Posts

Part of the JSON Toolkit
Pattern
{"if":{"properties":{"status":{"const":"published"}}},"then":{"required":["publishedAt"]},"else":{"not":{"required":["publishedAt"]}}}

one flat schema — publishedAt is required only when status is published, and forbidden otherwise

Explanation

A draft and a published blog post share almost every field — title, slug, author, tags — and differ in exactly one way: a published post needs a publish date, and a draft shouldn't have one yet. That's a much smaller difference than the card-vs-bank-transfer payment methods from the payment request guide, and splitting the whole schema into two branches over it would mean repeating every shared property in both. if/then/else exists for exactly this case: keep one flat schema, and only vary the constraints that actually depend on another field.

{
  "type": "object",
  "properties": {
    "title": { "type": "string", "minLength": 1, "maxLength": 120 },
    "slug": { "type": "string", "pattern": "^[a-z0-9]+(-[a-z0-9]+)*$" },
    "status": { "type": "string", "enum": ["draft", "published"] },
    "publishedAt": { "type": "string", "format": "date-time" },
    "tags": { "type": "array", "items": { "type": "string" }, "uniqueItems": true, "maxItems": 10 },
    "authorId": { "type": "string" }
  },
  "required": ["title", "slug", "status", "authorId"],
  "additionalProperties": false,
  "if": {
    "properties": { "status": { "const": "published" } },
    "required": ["status"]
  },
  "then": {
    "required": ["publishedAt"]
  },
  "else": {
    "not": { "required": ["publishedAt"] }
  }
}

if is itself a full schema, not a boolean condition — the instance is checked against it the same way it would be against any other subschema. When it validates (here, when status is "published"), the then schema is also applied on top of the base schema, adding publishedAt to what's required. When if doesn't validate — any other status value, including "draft" — the else schema applies instead. Both branches still have to satisfy every top-level constraint too; if/then/else only adds conditions, it never replaces the base schema the way a oneOf branch does.

"else": { "not": { "required": ["publishedAt"] } } is the part that surprises people the first time they need it. There's no keyword that directly means "this property must be absent" in every draft — required only checks presence, so wrapping it in not inverts that into exactly the constraint needed: a draft that already has a publishedAt — say, a post that was published and then reverted to draft without clearing the date — fails validation instead of silently keeping a stale timestamp.

The rule of thumb between the two patterns: reach for oneOf plus const when the shapes genuinely diverge — different required fields, different meaning, little in common. Reach for if/then/else when it's the same object with one or two fields whose constraints depend on another field's value, which is the far more common case in practice.

For the vocabulary behind enum, pattern, and uniqueItems, see What Is JSON Schema?. For if/then/else and other conditional keywords in more depth, see JSON Schema Validation Explained.

Valid examples

  • {"title":"How Recursion Works","slug":"how-recursion-works","status":"published","publishedAt":"2026-09-20T10:00:00Z","authorId":"auth_1","tags":["algorithms","recursion"]}

    status is published and publishedAt is present — satisfies the if/then branch.

  • {"title":"Draft: New API Design","slug":"draft-new-api-design","status":"draft","authorId":"auth_2"}

    status is draft and publishedAt is left out entirely — satisfies the else branch's not: {required: ["publishedAt"]}.

  • {"title":"Short Post","slug":"short-post","status":"published","publishedAt":"2026-01-01T00:00:00Z","authorId":"auth_4"}

    tags left out entirely — it's not required, so the object is still valid without it.

Invalid examples

  • {"title":"Missing Date","slug":"missing-date","status":"published","authorId":"auth_5"}

    status is published but publishedAt is missing — fails the if/then branch's required constraint.

  • {"title":"Backdated Draft","slug":"backdated-draft","status":"draft","publishedAt":"2026-09-20T10:00:00Z","authorId":"auth_6"}

    status is draft but publishedAt is present — fails the else branch's not: {required: ["publishedAt"]}, since a draft shouldn't have a publish date yet.

  • {"title":"Bad Slug","slug":"Bad Slug!","status":"draft","authorId":"auth_7"}

    slug contains spaces and uppercase letters — fails the slug pattern, which only allows lowercase letters, digits, and single hyphens between words.

Try it now