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.

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→1and3, excluding the nested2.
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:
- Tuple syntax changed: in draft-07 an array-valued
itemsexpressed a tuple; 2020-12 moved that toprefixItems; $refand sibling keywords: in draft-07 sibling keywords are ignored when$refis 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:
- 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;
- Turn the Pointer into plain language before showing it to end users;
/items/2/priceis 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-formatspackage, otherwiseformat: "email"is effectively inert; - Some validators treat
formatas an assertion enabled by default, so behavior is inconsistent; - Even when enabled, the
emailregex is very loose —a@bpasses.
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 |