This is an automated email from the ASF dual-hosted git repository.
wgtmac pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/iceberg-cpp.git
The following commit(s) were added to refs/heads/main by this push:
new ecf36247 fix(expression): return a parse error for non-string type
nodes in expression JSON (#982)
ecf36247 is described below
commit ecf36247c6abbfaee591a0c89f8c5055344e3a1b
Author: YangJie <[email protected]>
AuthorDate: Sun Oct 11 09:16:30 2026 -0400
fix(expression): return a parse error for non-string type nodes in
expression JSON (#982)
IsTransformTerm, NamedReferenceFromJson, both LiteralFromJson
overloads, and ExpressionFromJson read the 'type' discriminator with
json[kType].get<std::string>() guarded only by contains(), so a
present-but-non-string 'type' (e.g. a number) threw an uncaught
nlohmann type_error out of the Result-returning parse API. Check
is_string() first, matching the sibling OperationTypeFromJson guard,
so malformed expression JSON yields JsonParseError instead of an
escaped exception.
---
src/iceberg/expression/json_serde.cc | 13 +++++++------
src/iceberg/test/expression_json_test.cc | 27 +++++++++++++++++++++++++++
2 files changed, 34 insertions(+), 6 deletions(-)
diff --git a/src/iceberg/expression/json_serde.cc
b/src/iceberg/expression/json_serde.cc
index 1afed021..59f3fdcc 100644
--- a/src/iceberg/expression/json_serde.cc
+++ b/src/iceberg/expression/json_serde.cc
@@ -86,7 +86,7 @@ nlohmann::json MakeTransformJson(std::string_view
transform_str,
/// Helper to check if a JSON term represents a transform
bool IsTransformTerm(const nlohmann::json& json) {
- return json.is_object() && json.contains(kType) &&
+ return json.is_object() && json.contains(kType) && json[kType].is_string() &&
json[kType].get<std::string>() == kTransform && json.contains(kTerm);
}
@@ -228,8 +228,9 @@ nlohmann::json ToJson(const BoundTransform& transform) {
Result<std::unique_ptr<NamedReference>> NamedReferenceFromJson(
const nlohmann::json& json) {
- if (json.is_object() && json.contains(kType) &&
- json[kType].get<std::string>() == kReference && json.contains(kTerm)) {
+ if (json.is_object() && json.contains(kType) && json[kType].is_string() &&
+ json[kType].get<std::string>() == kReference && json.contains(kTerm) &&
+ json[kTerm].is_string()) {
return NamedReference::Make(json[kTerm].get<std::string>());
}
if (!json.is_string()) [[unlikely]] {
@@ -331,7 +332,7 @@ Result<int64_t> GetInt64Checked(const nlohmann::json& json)
{
Result<Literal> LiteralFromJson(const nlohmann::json& json, const Type* type) {
// If {"type": "literal", "value": <actual>} wrapper is present, unwrap it
first.
- if (json.is_object() && json.contains(kType) &&
+ if (json.is_object() && json.contains(kType) && json[kType].is_string() &&
json[kType].get<std::string>() == kLiteral && json.contains(kValue)) {
return LiteralFromJson(json[kValue], type);
}
@@ -499,7 +500,7 @@ Result<Literal> LiteralFromJson(const nlohmann::json& json,
const Type* type) {
Result<Literal> LiteralFromJson(const nlohmann::json& json) {
// Unwrap {"type": "literal", "value": <actual>} wrapper
- if (json.is_object() && json.contains(kType) &&
+ if (json.is_object() && json.contains(kType) && json[kType].is_string() &&
json[kType].get<std::string>() == kLiteral && json.contains(kValue)) {
return LiteralFromJson(json[kValue]);
}
@@ -619,7 +620,7 @@ Result<std::shared_ptr<Expression>>
ExpressionFromJson(const nlohmann::json& jso
SafeDumpJson(json));
}
- if (json[kType].get<std::string>() == kLiteral) {
+ if (json[kType].is_string() && json[kType].get<std::string>() == kLiteral) {
if (!json.contains(kValue) || !json[kValue].is_boolean()) [[unlikely]] {
return JsonParseError(
"Expression of type 'literal' must have a boolean 'value' field: {}",
diff --git a/src/iceberg/test/expression_json_test.cc
b/src/iceberg/test/expression_json_test.cc
index 1c993cc1..1afecc71 100644
--- a/src/iceberg/test/expression_json_test.cc
+++ b/src/iceberg/test/expression_json_test.cc
@@ -577,4 +577,31 @@ INSTANTIATE_TEST_SUITE_P(
return info.param.name;
});
+// A non-string "type" node must produce a parse error, not an uncaught
+// nlohmann type_error escaping the Result contract.
+TEST(ExpressionJsonTest, NonStringTypeIsParseError) {
+ EXPECT_THAT(ExpressionFromJson(R"({"type": 42, "term": "a"})"_json),
+ IsError(ErrorKind::kJsonParseError));
+ EXPECT_THAT(ExpressionFromJson(R"({"type": 42, "left": true, "right":
true})"_json),
+ IsError(ErrorKind::kJsonParseError));
+ EXPECT_THAT(LiteralFromJson(R"({"type": 42, "value": 1})"_json),
+ IsError(ErrorKind::kJsonParseError));
+
+ // A reference wrapper with a non-string term is likewise a parse error.
+ EXPECT_THAT(
+ ExpressionFromJson(
+
R"({"type":"eq","term":{"type":"reference","term":42},"value":1})"_json),
+ IsError(ErrorKind::kJsonParseError));
+
+ // A non-string "type" on a predicate's term node, routed through the
+ // transform-term check and then the named-reference parser.
+ EXPECT_THAT(
+
ExpressionFromJson(R"({"type":"eq","term":{"type":42,"term":"x"},"value":1})"_json),
+ IsError(ErrorKind::kJsonParseError));
+
+ // A non-string "type" on the type-aware LiteralFromJson overload.
+ EXPECT_THAT(LiteralFromJson(R"({"type":42,"value":1})"_json, int32().get()),
+ IsError(ErrorKind::kJsonParseError));
+}
+
} // namespace iceberg