JSON Toolkit

Query and Validation FAQ: JSONPath and Schema

Bottom line: Whether path queries should use JSONPath or Pointer, how to choose between Patch and Merge Patch, what Schema can block, and how to pick a draft version — all explained in one pass.

Intermediate~12 min readPublished 2026-10-30Updated 2026-10-30
Cover for the query and validation FAQ

Bottom line: use JSONPath to find nodes by condition, JSON Pointer or JSON Patch to address and edit exact locations, and Merge Patch to merge config; pin new Schemas to 2020-12 and turn on allErrors so every failure comes back in one pass.

Querying is "find it"; validation is "confirm it is right". Each has its own standard, so do not mix them.

1. Path queries

How should I choose between JSONPath and JSON Pointer?

It depends on whether you are "finding by condition" or "fetching by position".

Dimension JSONPath JSON Pointer
Standard RFC 9535 (2024) RFC 6901 (2013)
Expression $.users[?(@.age > 18)].id /users/0/id
Wildcards and recursion Supports *, .. Not supported
Conditional filtering Supported Not supported
Result A node set (possibly several) A single location
Expressiveness Strong, but with a heavy legacy of implementation differences Weak, but fully deterministic
Typical use Log extraction, debugging, data exploration JSON Patch, config addressing, error paths

The selection rule is straightforward:

  • Need to "get all nodes matching a condition" → JSONPath;
  • Need to "pin down one exact location and add, remove, or modify it" → JSON Pointer;
  • Use them together: find the node with JSONPath and describe its location with Pointer.

Do not use JSONPath for addressing in an API contract. JSONPath returns a set, and the three branches for empty, single-element, and multi-element results make the protocol complicated; Pointer means "either it points to an existing location or it does not exist", which suits a contract better.

What is the difference between $.. and $.*?

$.. is recursive descent and walks every level; $.* returns only the direct children of the current level.

{
  "a": { "id": 1, "child": { "id": 2 } },
  "b": { "id": 3 }
}
  • $.* → the two objects {id:1, child:{...}} and {id:3};
  • $..id → three values: 1, 2, 3;
  • $.*.id → 1 and 3, excluding the nested 2.

Recursive descent is handy but expensive: $.. walks the entire tree, which is O(n) on a large document, and it easily scoops up deep fields you did not intend. If a concrete level works, avoid ...

Can filters compare strings? Can they match with regex?

String comparison works; regex depends on the implementation.

Standard syntax (RFC 9535):

$.items[?(@.status == "active")]
$.items[?(@.price > 10 && @.stock > 0)]
$.items[?(@.name == $.target)]      // compare with another node

String comparison uses double quotes (single quotes are also allowed in RFC 9535, but some older implementations accept only double quotes). Numeric comparison follows numeric semantics; mixing a string with a number, such as "10" > 9, is undefined behavior in the standard, so do not rely on it.

For regex, RFC 9535 defines the match() and search() functions, but support for them varies considerably between implementations. The safe approach: let JSONPath do coarse filtering, and do the regex matching in code after you retrieve the results.

Should I use JSON Patch or Merge Patch?

Both describe "how to change something", but their expressiveness and semantics are completely different.

Dimension JSON Patch (RFC 6902) Merge Patch (RFC 7386)
Form An array of operations [{"op":"add",...}] A partial object
Supported operations add / remove / replace / move / copy / test Merge only (deletion via null)
Can address array elements Yes, by index No, the whole array is replaced
Atomicity test enables optimistic locking None
Document size Large but precise Small but limited in expressiveness
Good for Fine-grained edits, partial API updates Config merging, partial form submission

Decision criteria:

  • To change a field of the third element in an array → JSON Patch;
  • To merge several config entries into the existing config → Merge Patch;
  • To get concurrency control of "fail if the original value is not X" → JSON Patch's test.

Note the trap in Merge Patch's null semantics: null means "delete this key", not "set the value to null". Setting a value to null is impossible with Merge Patch.

2. Schema validation

What problems does JSON Schema solve?

It turns "what the data should look like" from a verbal agreement into a machine-checkable contract. The errors it can actually block:

  • Missing required fields (required);
  • Wrong type, for example a string where a number is expected (type);
  • Values out of range (minimum / maximum / minLength);
  • Values outside the allowed set (enum);
  • Inconsistent array element types or wrong array length (items / minItems);
  • Missing fields in nested structures (recursive validation through properties);
  • Extra fields (additionalProperties: false).

What it cannot solve: business rules ("the end time must be later than the start time"), cross-field conditions (some can be expressed with if/then, but readability suffers), database uniqueness, and permission checks.

So Schema is the gatekeeper for "data shape", not a replacement for business logic.

Should I use draft-07 or 2020-12?

Use 2020-12 for new projects; for existing systems, check what the validator supports.

Version Status Key changes
draft-04 Outdated Many older libraries still default to it
draft-07 De facto standard Widest support, richest ecosystem
2019-09 Transitional Introduced $defs, unevaluatedProperties
2020-12 Currently recommended items split into prefixItems + items, supports $dynamicRef

Two migration traps:

  1. Tuple syntax changed: in draft-07 an array-valued items expressed a tuple; 2020-12 moved that to prefixItems;
  2. $ref and sibling keywords: in draft-07 sibling keywords are ignored when $ref is present; 2020-12 allows them to coexist.

If the validator supports only draft-07, writing 2020-12 syntax anyway is silently ignored — and the most dangerous case is "validation passes" when nothing was validated at all.

How do I generate TypeScript types from a Schema?

Two routes: derive types from the Schema, or generate the Schema from types.

From Schema to types (backend defines the contract, frontend consumes it):

import type { FromSchema } from 'json-schema-to-ts';

const userSchema = {
  type: 'object',
  properties: {
    id: { type: 'string' },
    age: { type: 'integer' },
  },
  required: ['id'],
  additionalProperties: false,
} as const;

type User = FromSchema<typeof userSchema>;
// { id: string; age?: number }

The key is as const: without it, type: 'string' is inferred as string rather than the literal 'string', and inference breaks down.

From types to Schema (the frontend already has types and needs runtime validation):

import { z } from 'zod';
import { zodToJsonSchema } from 'zod-to-json-schema';

const userSchema = z.object({ id: z.string(), age: z.number().int().optional() });
const jsonSchema = zodToJsonSchema(userSchema);

The advantage of this route is that "one definition yields both types and runtime validation"; the cost is introducing a DSL, and the generated Schema may carry implementation details (for example an unattractive $ref structure).

How do I locate the specific field when validation fails?

Look at the instancePath (2020-12) or dataPath (draft-07) returned by the validator; it is a JSON Pointer to the error location.

[
  {
    "instancePath": "/items/2/price",
    "schemaPath": "#/properties/items/items/properties/price/type",
    "keyword": "type",
    "message": "must be number"
  }
]

instancePath: "/items/2/price" tells you directly: the price field of the third element in the items array has the wrong type.

Two practical suggestions:

  1. Return all errors at once instead of throwing on the first one — the frontend fixing everything in one pass is faster than back-and-forth attempts;
  2. Turn the Pointer into plain language before showing it to end users; /items/2/price is useful to developers and useless to users.

allErrors: true is the switch in most validators (Ajv requires turning it on explicitly); the default is often fail-fast mode.

Should additionalProperties: false be enabled?

Enable it for internal contracts; be cautious with public APIs.

Once enabled, any field not declared under properties fails validation. The benefit is catching typos (userNmae) and unexpected pass-through; the downside is that an upstream field addition breaks downstream consumers, destroying backward compatibility.

Compromise:

Scenario Recommendation
Between internal services Enable. Field changes go through version management
Public external APIs Disable. Allow upstream to add fields
Consuming third-party data Disable. You do not control the other side
Configuration validation Enable. A typo in config should fail immediately

How do I validate that array elements are unique?

Two different semantics — do not confuse them:

{ "uniqueItems": true }

This deduplicates the whole element, requiring that the array contains no two deeply equal elements.

{
  "type": "array",
  "items": { "type": "object", "properties": { "id": { "type": "string" } } },
  "uniqueItems": true
}

The snippet above only guarantees that the objects as a whole are not duplicated; {"id":"a","name":"x"} and {"id":"a","name":"y"} do not count as duplicates.

To deduplicate by a specific field, Schema cannot express it; you have to validate in the business layer:

const ids = items.map((item) => item.id);
if (new Set(ids).size !== ids.length) {
  throw new Error('id 重复');
}

Does format actually validate emails and dates?

Not necessarily. In the specification, format is an "annotation" by default rather than an "assertion", so a validator may collect it without validating it.

  • Ajv requires installing and configuring the separate ajv-formats package, otherwise format: "email" is effectively inert;
  • Some validators treat format as an assertion enabled by default, so behavior is inconsistent;
  • Even when enabled, the email regex is very loose — a@b passes.

Conclusion: do not treat format as strong validation. Do strict validation of emails, URLs, and dates in your business code; keeping format in the Schema as documentation is worthwhile, but do not expect it to stop dirty data.

3. Quick reference

Question Conclusion
Find nodes by condition JSONPath
Precise addressing, apply patches JSON Pointer
Change array elements JSON Patch, Merge Patch cannot do it
Merge configuration Merge Patch
Schema version for new projects 2020-12, mind prefixItems
Generate TS types from Schema json-schema-to-ts + as const
Locate validation errors Read instancePath, enable allErrors
Deduplicate by field Schema cannot; use a Set in the business layer
format: email Most validators do not check it by default, so do not rely on it