Summary
The expression JSON deserialization functions in src/iceberg/expression/json_serde.cc call json[kType].get<std::string>() (and one json[kTerm].get<std::string>()) guarded only by is_object() and contains(kType). When "type" or "term" is a number, bool, null, or array, nlohmann throws json::type_error.302. These functions return Result<...> and the expression parse chain has no try/catch, so the exception escapes the Result contract and terminates any caller that lacks an exception barrier.
Root Cause
The unguarded get<std::string>() calls sit behind only is_object() && contains(kType): IsTransformTerm, NamedReferenceFromJson (both the type and the term node), both LiteralFromJson overloads' wrapper check, and ExpressionFromJson. The sibling OperationTypeFromJson already checks is_string() first, and util/json_util_internal.h exists precisely to convert nlohmann throws into JsonParseError. The unguarded sites are the outliers, not the policy.
Impact
These parsers run on real deserialization paths, all through ICEBERG_ASSIGN_OR_RAISE, which forwards a Result error but not a thrown exception. Table-metadata parsing is the live path today: FieldFromJson (src/iceberg/json_serde.cc) parses a field's initial-default / write-default through the type-aware LiteralFromJson, so a metadata file whose default is wrapped as {"type": <non-string>, "value": ...} throws instead of returning a parse error — the same path #877/#878 hardened. REST catalog responses reach the expression sites too: the scan-metrics report filter (metrics/json_serde.cc) and the residual, partition, and plan filters (catalog/rest/json_serde.cc) all parse server-supplied JSON. A non-string discriminator on any of these throws type_error.302 out of a Result-returning function and past the ICEBERG_ASSIGN_OR_RAISE call site, which is the same uncaught-exception-escaping-Result class as the merged #857.
Per SECURITY-THREAT-MODEL.md, catalog-supplied metadata is trusted input, so this is a robustness and contract issue, not a security one.
Proposed Fix
Add an is_string() check to each guard so a non-string type/term returns JsonParseError, mirroring OperationTypeFromJson.
Summary
The expression JSON deserialization functions in
src/iceberg/expression/json_serde.cccalljson[kType].get<std::string>()(and onejson[kTerm].get<std::string>()) guarded only byis_object()andcontains(kType). When"type"or"term"is a number, bool, null, or array, nlohmann throwsjson::type_error.302. These functions returnResult<...>and the expression parse chain has notry/catch, so the exception escapes theResultcontract and terminates any caller that lacks an exception barrier.Root Cause
The unguarded
get<std::string>()calls sit behind onlyis_object() && contains(kType):IsTransformTerm,NamedReferenceFromJson(both thetypeand thetermnode), bothLiteralFromJsonoverloads' wrapper check, andExpressionFromJson. The siblingOperationTypeFromJsonalready checksis_string()first, andutil/json_util_internal.hexists precisely to convert nlohmann throws intoJsonParseError. The unguarded sites are the outliers, not the policy.Impact
These parsers run on real deserialization paths, all through
ICEBERG_ASSIGN_OR_RAISE, which forwards aResulterror but not a thrown exception. Table-metadata parsing is the live path today:FieldFromJson(src/iceberg/json_serde.cc) parses a field'sinitial-default/write-defaultthrough the type-awareLiteralFromJson, so a metadata file whose default is wrapped as{"type": <non-string>, "value": ...}throws instead of returning a parse error — the same path #877/#878 hardened. REST catalog responses reach the expression sites too: the scan-metrics report filter (metrics/json_serde.cc) and the residual, partition, and plan filters (catalog/rest/json_serde.cc) all parse server-supplied JSON. A non-string discriminator on any of these throwstype_error.302out of aResult-returning function and past theICEBERG_ASSIGN_OR_RAISEcall site, which is the same uncaught-exception-escaping-Resultclass as the merged #857.Per
SECURITY-THREAT-MODEL.md, catalog-supplied metadata is trusted input, so this is a robustness and contract issue, not a security one.Proposed Fix
Add an
is_string()check to each guard so a non-stringtype/termreturnsJsonParseError, mirroringOperationTypeFromJson.