From fcd4b2d55ebca03767adf57976c9daf4ed38adc9 Mon Sep 17 00:00:00 2001 From: "cemre.mengu" Date: Sun, 30 Aug 2026 21:13:25 +0300 Subject: [PATCH 1/2] ignore null objects --- .../src/doc_mapper/doc_mapper_impl.rs | 111 ++++++++++++++++++ .../src/doc_mapper/mapping_tree.rs | 6 + 2 files changed, 117 insertions(+) diff --git a/quickwit/quickwit-doc-mapper/src/doc_mapper/doc_mapper_impl.rs b/quickwit/quickwit-doc-mapper/src/doc_mapper/doc_mapper_impl.rs index 087fe2839ab..d6a5f8a7f39 100644 --- a/quickwit/quickwit-doc-mapper/src/doc_mapper/doc_mapper_impl.rs +++ b/quickwit/quickwit-doc-mapper/src/doc_mapper/doc_mapper_impl.rs @@ -899,6 +899,117 @@ mod tests { Ok(()) } + #[test] + fn test_parse_document_with_null_object() { + use serde::Deserialize; + + let doc_mapper_json = json!({ + "field_mappings": [ + { + "name": "payload", + "type": "object", + "field_mappings": [ + { + "name": "head", + "type": "object", + "field_mappings": [ + {"name": "label", "type": "text"}, + { + "name": "repo", + "type": "object", + "field_mappings": [ + {"name": "name", "type": "text"} + ] + } + ] + } + ] + } + ] + }); + let doc_mapper = DocMapperBuilder::deserialize(doc_mapper_json) + .unwrap() + .try_build() + .unwrap(); + + // A `null` object is simply ignored, like a `null` leaf value. + let JsonValue::Object(json_obj) = json!({ + "payload": { + "head": { + "label": "main", + "repo": JsonValue::Null + } + } + }) else { + panic!("expected a JSON object"); + }; + let (_, document) = doc_mapper.doc_from_json_obj(json_obj, 0).unwrap(); + let schema = doc_mapper.schema(); + let label_field = schema.get_field("payload.head.label").unwrap(); + assert_eq!( + document + .get_first(label_field) + .and_then(|value| value.as_str()), + Some("main") + ); + + // The whole document can also be `null` all the way up. + let JsonValue::Object(json_obj) = json!({"payload": JsonValue::Null}) else { + panic!("expected a JSON object"); + }; + doc_mapper.doc_from_json_obj(json_obj, 0).unwrap(); + + // Non-null, non-object values are still rejected. + let JsonValue::Object(json_obj) = json!({"payload": {"head": {"repo": "quickwit"}}}) else { + panic!("expected a JSON object"); + }; + let error = doc_mapper.doc_from_json_obj(json_obj, 0).unwrap_err(); + assert!(matches!(error, DocParsingError::ValueError(field_path, _) + if field_path == "payload.head.repo")); + } + + #[test] + fn test_validate_document_with_null_object() { + use serde::Deserialize; + + let doc_mapper_json = json!({ + "mode": "strict", + "field_mappings": [ + { + "name": "payload", + "type": "object", + "field_mappings": [ + { + "name": "repo", + "type": "object", + "field_mappings": [ + {"name": "name", "type": "text"} + ] + } + ] + } + ] + }); + let doc_mapper = DocMapperBuilder::deserialize(doc_mapper_json) + .unwrap() + .try_build() + .unwrap(); + + let json_val: serde_json_borrow::Value = + serde_json::from_str(r#"{"payload": {"repo": null}}"#).unwrap(); + doc_mapper + .validate_json_obj(json_val.as_object().unwrap()) + .unwrap(); + + let json_val: serde_json_borrow::Value = + serde_json::from_str(r#"{"payload": {"repo": "quickwit"}}"#).unwrap(); + let error = doc_mapper + .validate_json_obj(json_val.as_object().unwrap()) + .unwrap_err(); + assert!(matches!(error, DocParsingError::ValueError(field_path, _) + if field_path == "payload.repo")); + } + #[test] fn test_timestamp_field_in_object_is_valid() { serde_json::from_str::( diff --git a/quickwit/quickwit-doc-mapper/src/doc_mapper/mapping_tree.rs b/quickwit/quickwit-doc-mapper/src/doc_mapper/mapping_tree.rs index 30b16fea4c8..f08c5c7a476 100644 --- a/quickwit/quickwit-doc-mapper/src/doc_mapper/mapping_tree.rs +++ b/quickwit/quickwit-doc-mapper/src/doc_mapper/mapping_tree.rs @@ -945,6 +945,9 @@ impl MappingTree { mapping_leaf.validate_from_json(json_value, field_path) } MappingTree::Node(mapping_node) => { + if json_value.is_null() { + return Ok(()); + } if let Some(json_obj) = json_value.as_object() { mapping_node.validate_from_json(json_obj, strict_mode, field_path) } else { @@ -970,6 +973,9 @@ impl MappingTree { mapping_leaf.doc_from_json(json_value, document, path) } MappingTree::Node(mapping_node) => { + if json_value.is_null() { + return Ok(()); + } if let JsonValue::Object(json_obj) = json_value { mapping_node.doc_from_json(json_obj, mode, document, path, dynamic_json_obj) } else { From c602ea3f229ad9411bb0b3c35ebd39fb677cdb0f Mon Sep 17 00:00:00 2001 From: Cemre Mengu Date: Thu, 15 Oct 2026 19:24:31 +0300 Subject: [PATCH 2/2] update docs --- docs/configuration/index-config.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/configuration/index-config.md b/docs/configuration/index-config.md index 876311e96cb..50773d375a2 100644 --- a/docs/configuration/index-config.md +++ b/docs/configuration/index-config.md @@ -430,6 +430,8 @@ To declare an array type of `i64` in the index config, you just have to set the Quickwit supports nested objects as long as it does not contain arrays of objects. +A `null` value for an `object` field is ignored along with all of its subfields (see [Behavior with null values or missing fields](#behavior-with-null-values-or-missing-fields)). + ```yaml name: resource type: object @@ -599,7 +601,7 @@ For field names containing the `.` character, you will need to escape it when re ### Behavior with null values or missing fields -Fields with `null` or missing fields in your JSON document will be silently ignored when indexing. +Fields with `null` or missing fields in your JSON document will be silently ignored when indexing. This applies to every field type, including `object` fields: when an `object` field is `null`, it and all of its subfields are skipped, and the rest of the document is indexed normally. This is true in all modes, including `strict`. ## Indexing settings