Skip to content

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.

{ "Search": { /* query node */ } }
{ "Match": { /* 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.

{ "Array": { "Value": { /* query applied to each 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:

Object → Value → Object → Value → String

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.

{
  "Condition": {
    "HasKey":   { "Value": "action" },
    "HasValue": { "Value": "ApplyDiscount" }
  }
}
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 String node: the matched substring (or the RegexSubgroup capture, if set).
  • An Object node with a Key and no Value: 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" ObjectKey: record
"then into key discount" Value: { Object: { Key: discount } }
"the value at amount" Value: { Object: { Key: amount } }
"for every element of the list" ArrayValue: …
"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.


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.

{
  "Match": {
    "Object": {
      "Key": { "Value": "prompt" }
    }
  }
}

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.

{
  "Search": {
    "Object": {
      "Key": { "Regex": "(?i)api.?key" }
    }
  }
}

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]"}.

{
  "Search": {
    "String": {
      "Regex": "Bearer\\s+([A-Za-z0-9._-]+)",
      "RegexSubgroup": 1
    }
  }
}

11 · Bare regex Search — find a pattern anywhere

"Detect any SSN-shaped string anywhere in the body, regardless of structure." The simplest possible definition.

{
  "Search": {
    "String": { "Regex": "\\d{3}-\\d{2}-\\d{4}" }
  }
}

Testing a definition

Don't guess — validate against the real body you captured:

  • Dynamic Scan APIPOST the 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:

  1. Search vs Match — using Match on a body where the value isn't anchored at the root is the most common miss. Switch to Search.
  2. A missing Array step — there's a [ … ] in the path you didn't account for (only an issue under Match; Search recurses through it).
  3. Key vs Value swapped, or a typo in a key name (names match case-insensitively for Value, but must otherwise be exact).
  4. The id gotcha — if the value lives under a key named id, Search skipped it; anchor explicitly with Match.
  5. Regex case — a Regex is case-sensitive; add (?i) or switch to a Value exact 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.