In Rego, how do you assert on a JSON blob that arrives as a string field?
answer
- the engine sees a scalar, not a structure
- you have to open it yourself
- json, yaml, base64 all have one
- a built-in error is undefined, not a crash
- never sort version strings lexically
basics
~20 sDecode it first: json.unmarshal turns the string into a value you can reference, and yaml.unmarshal does the same for embedded YAML. Until you decode, traversal sees one opaque string and finds none of the fields inside it.
solid answer
~50 sA policy engine only sees the document it was handed. If a field's value is a *string* that happens to contain JSON — an image-metadata blob carried as an annotation, say — then `walk` and every path reference treat it as a scalar, and a rule looking for fields inside it silently matches nothing. `json.unmarshal(s)` parses the string into an object you can then reference or walk; `yaml.unmarshal` does the same for embedded YAML, and `base64.decode` comes first if the blob is encoded. Guard the decode: a built-in that errors makes the expression undefined by default rather than failing loudly, so a malformed blob turns into a silent pass. `json.is_valid(s)` as a precondition, or running the engine with strict built-in errors, is what turns that into something you can see. Once decoded, compare version fields with `semver.compare` rather than string comparison, and turn timestamps into integers with `time.parse_rfc3339_ns`.
code
rego · 15 linespackage example
meta := json.unmarshal(blob) if {
blob := input.metadata.annotations["build.metadata"]
json.is_valid(blob)
}
warn contains "build tool older than 1.10.0" if {
semver.compare(meta.tool_version, "1.10.0") < 0
}
warn contains "build metadata is present but unparseable" if {
blob := input.metadata.annotations["build.metadata"]
not json.is_valid(blob)
}go deeper
Know that a JSON or YAML payload carried as a string field has to be decoded with json.unmarshal or yaml.unmarshal before any field inside it can be referenced.
Explain that a built-in error becomes undefined rather than an error, and name the guards: json.is_valid, yaml.is_valid, semver.is_valid before the comparison.
Show that you write the unparseable-blob rule alongside the bad-value rule, so a payload nobody can read shows up as a finding instead of as silence.
Own the input contract. If a team keeps shipping structured data as an opaque string field, the durable fix is to change what the pipeline hands the engine, not to grow more decoding in every rule.
## The blind spot Rego evaluates over a parsed document. Anything that is a string in that document is, to the engine, a scalar with no interior. This bites whenever metadata travels as an embedded blob: a JSON object stuffed into an annotation value, a rendered manifest carried as a string field on a custom resource, a signed image-metadata payload attached alongside a bundle. A rule that references `blob.version` when `blob` is a string is undefined; a rule that `walk`s the outer document never descends into it. Both produce nothing, and nothing looks like a pass. ## Decoding - `json.unmarshal(s)` parses a JSON string into a Rego value — object, array or scalar. - `yaml.unmarshal(s)` does the same for YAML, which matters because embedded manifests are usually YAML rather than JSON. - `base64.decode(s)` first when the payload has been base64-encoded on the way in. After decoding you have an ordinary value: reference it by path, iterate it, or hand it to `walk` if the interesting field could be at any depth inside it. ## The failure everyone hits once Built-in errors in Rego do **not**, by default, abort the query. When a built-in is given input it cannot handle — `json.unmarshal` on a string that is not valid JSON, `semver.compare` on something that is not a semantic version — the expression becomes undefined, the rule body stops, and the rule yields no result. The malformed input therefore produces no finding, which is the opposite of what you want from a policy. Two defences, and they are complementary: 1. **Guard the input.** `json.is_valid(s)` (and `yaml.is_valid`) return a boolean you can test before decoding, and `semver.is_valid` does the same for versions. Then write a second rule that reports the *unparseable* case as a finding in its own right, so a blob nobody can read is visible rather than absent. 2. **Make errors loud where you can.** OPA can be run so that built-in errors are strict rather than silently undefined. That converts "nothing happened" into a failure you notice, which is what you want in the policy repo's own test run even if you would not want it in a live gate. ## Comparing what you decoded Two built-in families come up constantly once the blob is open: **Versions.** `semver.compare(a, b)` returns `-1`, `0` or `1` for a less-than, equal, greater-than ordering of two semantic version strings. Use it. String comparison gets `"1.9.0"` and `"1.10.0"` backwards, because lexically `"9"` sorts after `"1"`, and that inversion is a classic wrong answer — a rule meant to require at least `1.10.0` happily accepts `1.9.0`. `semver.is_valid(s)` screens strings that are not versions at all before you compare, since a comparison against a non-version is undefined rather than false. **Timestamps.** `time.parse_rfc3339_ns(s)` converts an RFC 3339 timestamp string into nanoseconds since the epoch, an integer you can compare with `<` and `>` against another parsed timestamp or a threshold supplied to the policy. Never compare timestamp strings directly unless you are certain they are identically formatted and zoned; parse them into integers and compare numbers. Alongside those sit the string and hashing families you reach for on decoded content: `regex.match(pattern, value)` for shape checks on identifiers, `crypto.sha256(s)` for a hex digest of a string, and the `crypto.x509` parsers for certificate material. Use the narrowest one that answers the question — a `regex.match` where a plain equality or a set membership would do is harder to review and easier to get subtly wrong. ## Putting it together The shape of a robust rule over an embedded blob is: check the string is valid, decode it, pull the field, convert it to a comparable type, compare. Four of those five steps can go undefined, and each one that does turns a finding into a silence. Writing the "could not read it" rule alongside the "read it and it was bad" rule is what keeps the silence from being indistinguishable from a clean result. ## What an interviewer is checking That you know parsing is your job, not the engine's; that you know a built-in error is undefined rather than a crash; and that you reach for `semver.compare` and `time.parse_rfc3339_ns` instead of comparing strings that happen to look sortable.
- What happens if json.unmarshal is handed a string that is not valid JSON?By default the built-in error makes the expression undefined, so the rule body stops and produces no result — the malformed blob passes silently. Screen the string with `json.is_valid` first and report the unparseable case as its own finding, and consider running with strict built-in errors in the policy repo's own tests.
- Why not just compare version strings with < and >?Because string ordering is lexical: `"1.9.0"` compares as greater than `"1.10.0"`, so a rule requiring at least 1.10.0 would accept 1.9.0. `semver.compare(a, b)` returns -1, 0 or 1 using semantic version ordering. Screen inputs with `semver.is_valid` first, since comparing a non-version is undefined.
- The blob is YAML rather than JSON — does anything change?Use `yaml.unmarshal` and `yaml.is_valid` instead; the structure of the rule is identical. It matters more often than you expect, because embedded manifests are usually authored as YAML, and a `json.unmarshal` against them goes undefined rather than complaining.
A sealed envelope inside a filing cabinet. Searching the cabinet finds the envelope, never its contents — you have to open it before any of the words inside can match anything.
saying these in an interview costs you the question
- Expects walk to descend into a string containing JSON
- Thinks a built-in error aborts the query
- Compares version strings with < and >
- Compares RFC 3339 timestamps as strings
- Never writes a rule for the unparseable blob