JSON Search DSL — Full Reference
When a Custom Data Type needs to detect a value inside JSON (REST API payloads and GraphQL alike), you describe what to look for and where with a small declarative grammar: the JSON Search DSL. This page is the complete reference for that grammar — every node, every field, the exact matching rules, and a gallery of working examples.
What this page is
This is the deep reference. If you just want the end-to-end workflow — capture traffic, find the request, build the detector — start with Custom Data Types and come back here when you need the full grammar. Every behavior documented here is taken from the Shield rule engine's JSON matcher, so it reflects exactly what the server does at runtime.
Mental model
A JSON Search definition is a tree of query nodes that Shield walks against the tree of the JSON body. Where a query node "lands" on a scalar (a string, number, or boolean), that scalar becomes a detected value — the thing Shield records, masks, or blocks.
YOUR QUERY (the DSL) THE BODY (from the HAR)
──────────────────── ───────────────────────
Search {
└ Object "action": "ApplyDiscount",
├ Condition (pin action) "record": {
├ Key: "record" ───► "discount": {
└ Value: Object "amount": 17, ◄── detected value
└ Key: "discount" "currency": "USD"
└ Value: Object }
└ Key: "amount" ──► }
You build that tree by answering three questions about the body, which map one-to-one onto the grammar:
| Question | DSL piece |
|---|---|
| Which message / object is this? (pin the operation) | Condition |
| What path leads to the value? | Object / Array chained by Value |
| What does the value itself look like? | String |
The envelope: Search vs Match
Every definition is a single JSON object with exactly one top-level key — Search or
Match — whose value is a query node.
| Top key | How it walks the body | Choose when |
|---|---|---|
Search |
Recursive descent. Applies the query at the root and every nested object and array element, at any depth. | You don't know (or don't care) exactly where the value sits, or the path varies. The default. |
Match |
Root-anchored. Applies the query only against the top of the document; the structure must line up from the root downward. | The body shape is fixed and you want to anchor the whole path from the top. |
When in doubt, use Search
Because Search recurses, you can often skip writing the outer Array / Object steps and
let it find the matching object wherever it lives. Match is stricter — every step from the
root must be spelled out — which is what you want for tightly-structured payloads (e.g. an AI
chat body whose messages[] array sits at the root).
Search skips keys named id
During recursive descent, Search does not descend into a property whose key is exactly
id. This avoids masking record identifiers by accident. If the value you need genuinely
lives under a key called id, anchor to it explicitly with Match and an Object/Key
path rather than relying on recursion.
Query nodes
A query node is a JSON object holding exactly one of the following keys. Nodes nest to form the path.
| Node | Meaning |
|---|---|
String |
Test a scalar (string / number / boolean) value. |
Object |
Optionally filter an object, then descend into one of its keys. |
Array |
Apply a query to each element of an array. |
Two more keys appear inside an Object node:
| Key | Meaning |
|---|---|
Condition |
A boolean pre-filter on the surrounding object. |
Value |
The polymorphic slot holding the next node in the path. |
String
Matches a scalar value and selects what gets detected/masked. A String node tests against
JSON strings, numbers, and booleans (a number 17 is matched as the text "17",
true/false as "true"/"false"). It never matches objects, arrays, or null.
{ "String": { "Value": "user" } } // exact, case-insensitive
{ "String": { "Regex": "\\d{3}-\\d{2}-\\d{4}" } } // regex (SSN)
{ "String": { "Regex": "Bearer\\s+([\\w.-]+)", "RegexSubgroup": 1 } } // capture group 1
| Field | Type | Meaning |
|---|---|---|
Value |
string | Exact match. Case-insensitive. When set, Regex is ignored. |
Regex |
string | Regular-expression match (Go RE2 — see note). The matched span becomes the value. |
RegexSubgroup |
int | Which capture group is the detected value. 0 (default) = the whole match; 1 = first group, etc. |
RegexSubgroup is what lets you match a value in context but mask only part of it. The pattern
Bearer\s+([\w.-]+) with RegexSubgroup: 1 detects the token only, leaving the literal
Bearer prefix untouched.
Regex flavor — Go RE2
Patterns compile with Go's regexp package (RE2). Practical consequences:
- No look-around and no back-references — RE2 doesn't support them.
- Use inline flags like
(?i)for case-insensitive,(?s)for dot-matches-newline. - In JSON, backslashes are doubled:
\\d,\\w,\\.. .+is the idiomatic "match any non-empty scalar" pattern (used to grab a whole field value regardless of content).
Note the asymmetry: Value is case-insensitive, but Regex is case-sensitive unless you
add (?i).
Object
Descend into a property of an object. Optionally filter the object first with a Condition.
{
"Object": {
"Condition": { /* optional pre-filter on THIS object */ },
"Key": { "Value": "email" }, // property NAME to enter (a String matcher)
"Value": { /* the next query node for that property's value */ }
}
}
| Field | Meaning |
|---|---|
Condition |
(optional) An object condition that must hold before Shield enters the key. |
Key |
A String matcher applied to the property name (supports Value or Regex). |
Value |
(optional) A Value node applied to the property's value. |
If Value is omitted, the matched key's value becomes the detected value — provided it's a
scalar. So { "Object": { "Key": { "Value": "accountId" } } } detects and masks whatever scalar
sits at accountId. (If that value is itself an object or array, nothing is captured — you have
to keep descending with a Value.)
This omit-the-Value shorthand is the most common leaf in real policies: walk to the key you
care about, stop, and let its value be the result.
Array
Step into a list and apply a query to every element.
| Field | Meaning |
|---|---|
Value |
A Value node evaluated against each element of the array. |
Use an Array node every time the body shows [ … ] between you and the value — a list of
users, messages, line items. With Match you must spell out every array hop; with Search you
can often let recursion handle it.
Value
Value is the polymorphic slot that chains the path. Inside an Object or Array, it holds
the next query node — another Object, an Array, or a leaf String:
{ "Value": { "String": { "…": "…" } } } // leaf — the value is a scalar to match
{ "Value": { "Object": { "…": "…" } } } // descend further into an object
{ "Value": { "Array": { "…": "…" } } } // descend into an array
A full path is just these nodes nested through Value:
Condition
A Condition filters an object by what it contains. It's how you say "only the
ApplyDiscount request" or "only elements where type == "contact"" — without it, a Search
would match the same shape everywhere it appears.
| Field | Type | Meaning |
|---|---|---|
HasKey |
String matcher |
The object has a key whose name matches. |
HasValue |
String matcher |
The object has a property whose value matches. |
IsNull |
boolean | true → the value is null; false → it is not null. |
And |
array of conditions | All listed conditions must hold. |
Or |
array of conditions | At least one listed condition must hold. |
Not |
a single condition | The inner condition must not hold. |
HasKey + HasValue must be satisfied by the same property
When both are present, they don't just mean "this key exists somewhere and that value
exists somewhere." They must be satisfied by one and the same key/value pair. That's the
standard "pin to exactly one request" idiom: HasKey: action and
HasValue: ApplyDiscount means "there is a property literally named action
whose value is ApplyDiscount." (For GraphQL, pin operationName the same way.)
And, Or, and Not compose, so you can build richer filters — e.g. "value is one of two
operations" (Or) or "any object that is not an admin record" (Not).
What gets detected (and masked)
The detected value — what shows up in the activity log and what a mask/block acts on — is the scalar a query node lands on:
- A
Stringnode: the matched substring (or theRegexSubgroupcapture, if set). - An
Objectnode with aKeyand noValue: the scalar value of that key.
A single definition can produce multiple matches in one body — every array element or repeated object that satisfies the query is detected independently. When Shield obfuscates, it replaces each detected span in place and re-serializes the body, leaving everything else byte-for-byte intact (numbers stay numbers, surrounding structure is untouched).
Translating a body into the DSL
Take the plain-English path you wrote while analyzing the HAR and map it mechanically:
| You said… | DSL node |
|---|---|
"only the ApplyDiscount request" |
Condition with HasKey + HasValue |
"go into key record" |
Object → Key: record |
"then into key discount" |
Value: { Object: { Key: discount } } |
"the value at amount" |
Value: { Object: { Key: amount } } |
| "for every element of the list" | Array → Value: … |
| "match any non-empty value" | Value: { String: { Regex: ".+" } } |
The deepest Key you leave without a Value (or whose Value is a String matcher) is the
value Shield detects and masks.
Example gallery
Every example below is a complete, valid definition. The first set are the canonical real-world patterns; the rest are focused snippets for each grammar feature.
1 · Pin a request by action, detect a nested field
"In the ApplyDiscount request, detect record.discount.amount." Single value, no
arrays → a straight Object traversal with a Condition to pin the action. This is the
worked example policy. (For a GraphQL API, pin operationName the same
way.)
{
"Search": {
"Object": {
"Condition": {
"HasKey": { "Value": "action" },
"HasValue": { "Value": "ApplyDiscount" }
},
"Key": { "Value": "record" },
"Value": {
"Object": {
"Key": { "Value": "discount" },
"Value": { "Object": { "Key": { "Value": "amount" } } }
}
}
}
}
}
2 · Detect a value with no further descent (leaf shorthand)
"In the UpdateAccountStanding GraphQL operation, detect variables.accountId." Note the final
Object has only a Key — its scalar value is what gets detected.
{
"Search": {
"Object": {
"Condition": {
"HasKey": { "Value": "operationName" },
"HasValue": { "Value": "UpdateAccountStanding" }
},
"Key": { "Value": "variables" },
"Value": {
"Object": {
"Key": { "Value": "accountId" }
}
}
}
}
}
3 · A field inside every element of an array (conditional)
"In the contact-list response, for each object whose type == "contact", the email is at
properties.email." Because Search recurses, you don't even have to spell out the surrounding
array — it visits every object and applies the condition.
{
"Search": {
"Object": {
"Condition": {
"HasKey": { "Value": "type" },
"HasValue": { "Value": "contact" }
},
"Key": { "Value": "properties" },
"Value": { "Object": { "Key": { "Value": "email" } } }
}
}
}
4 · Free text from a deeply nested array (AI prompt)
"ChatGPT's prompt is at messages[].content.parts[]." The shape is fixed from the root, so use
Match and spell out both array hops — one for messages[], one for parts[].
{
"Match": {
"Object": {
"Key": { "Value": "messages" },
"Value": {
"Array": {
"Value": {
"Object": {
"Key": { "Value": "content" },
"Value": {
"Object": {
"Key": { "Value": "parts" },
"Value": { "Array": { "Value": { "String": { "Regex": ".+" } } } }
}
}
}
}
}
}
}
}
}
5 · A simple top-level field
"The prompt is the top-level prompt field." (Claude's chat API.) No condition, no descent —
just grab the value of one key.
6 · Match a key by regex, anywhere in the body
"Detect the value of any key that looks like apiKey / api_key / api-key." The Key
matcher takes a Regex too.
7 · Or — one of several operations
"Detect the email field when the operation is either CreateUser or UpdateUser."
{
"Search": {
"Object": {
"Condition": {
"Or": [
{ "HasValue": { "Value": "CreateUser" } },
{ "HasValue": { "Value": "UpdateUser" } }
]
},
"Key": { "Value": "email" },
"Value": { "String": { "Regex": ".+@.+" } }
}
}
}
8 · Not — exclude a case
"Detect the email field on any object that is not an admin record" (i.e. no property has
the value admin).
{
"Search": {
"Object": {
"Condition": {
"Not": { "HasValue": { "Value": "admin" } }
},
"Key": { "Value": "email" },
"Value": { "String": { "Regex": ".+@.+" } }
}
}
}
9 · HasKey — only objects that have a given field
"Detect email only on objects that actually carry an email key" (skip records that omit
it). The condition gates the match; the Key/Value does the detection.
{
"Search": {
"Object": {
"Condition": { "HasKey": { "Value": "email" } },
"Key": { "Value": "email" },
"Value": { "String": { "Regex": ".+@.+" } }
}
}
}
10 · RegexSubgroup — mask only part of a value
"Detect a bearer token but keep the Bearer prefix visible." Against
{"auth":"Bearer eyJhbGciOi.payload.sig"} this detects only eyJhbGciOi.payload.sig, so masking
yields {"auth":"Bearer [REDACTED]"}.
11 · Bare regex Search — find a pattern anywhere
"Detect any SSN-shaped string anywhere in the body, regardless of structure." The simplest possible definition.
Testing a definition
Don't guess — validate against the real body you captured:
- Dynamic Scan API —
POSTthe captured body together with the data type and check the response shows the value detected/obfuscated. - Re-fire through the proxy — replay the captured request with the rule live (e.g. from Postman) and confirm the activity log shows the detection.
If nothing matches, work through this checklist in order:
SearchvsMatch— usingMatchon a body where the value isn't anchored at the root is the most common miss. Switch toSearch.- A missing
Arraystep — there's a[ … ]in the path you didn't account for (only an issue underMatch;Searchrecurses through it). KeyvsValueswapped, or a typo in a key name (names match case-insensitively forValue, but must otherwise be exact).- The
idgotcha — if the value lives under a key namedid,Searchskipped it; anchor explicitly withMatch. - Regex case — a
Regexis case-sensitive; add(?i)or switch to aValueexact match.
Field-name quick reference
All keys are PascalCase and case-sensitive (this is the DSL's own syntax, distinct from the case-insensitive value matching above).
| Context | Keys |
|---|---|
| Top level | Search, Match (one only); Datatype (optional, ignored) |
String node |
Value, Regex, RegexSubgroup |
Object node |
Condition, Key, Value |
Array node |
Value |
Condition |
HasKey, HasValue, IsNull, And, Or, Not |
Back to Custom Data Types · or the overview.