From 8e6e92b6cdfd478e3025ce52af3248c7d329a2f8 Mon Sep 17 00:00:00 2001 From: naomichi-y Date: Wed, 8 May 2024 19:15:04 +0900 Subject: [PATCH 1/6] Fix parseToken array popping Signed-off-by: naomichi-y --- .../xcontent/JsonToStringXContentParser.java | 28 +++++++++++-------- .../JsonToStringXContentParserTests.java | 21 ++++++++++++++ 2 files changed, 37 insertions(+), 12 deletions(-) diff --git a/server/src/main/java/org/opensearch/common/xcontent/JsonToStringXContentParser.java b/server/src/main/java/org/opensearch/common/xcontent/JsonToStringXContentParser.java index 998122d9e5c43..1869c809400ce 100644 --- a/server/src/main/java/org/opensearch/common/xcontent/JsonToStringXContentParser.java +++ b/server/src/main/java/org/opensearch/common/xcontent/JsonToStringXContentParser.java @@ -65,7 +65,7 @@ public JsonToStringXContentParser( public XContentParser parseObject() throws IOException { builder.startObject(); StringBuilder path = new StringBuilder(fieldTypeName); - parseToken(path, null); + parseToken(path, null, true); builder.field(this.fieldTypeName, keyList); builder.field(this.fieldTypeName + VALUE_SUFFIX, valueList); builder.field(this.fieldTypeName + VALUE_AND_PATH_SUFFIX, valueAndPathList); @@ -74,8 +74,7 @@ public XContentParser parseObject() throws IOException { return JsonXContent.jsonXContent.createParser(this.xContentRegistry, this.deprecationHandler, String.valueOf(jString)); } - private void parseToken(StringBuilder path, String currentFieldName) throws IOException { - + private void parseToken(StringBuilder path, String currentFieldName, boolean popName) throws IOException { while (this.parser.nextToken() != Token.END_OBJECT) { if (this.parser.currentName() != null) { currentFieldName = this.parser.currentName(); @@ -100,16 +99,18 @@ private void parseToken(StringBuilder path, String currentFieldName) throws IOEx this.keyList.add(fieldNameSuffix); } } else if (this.parser.currentToken() == Token.START_ARRAY) { - parseToken(path, currentFieldName); + parseToken(path, currentFieldName, false); break; } else if (this.parser.currentToken() == Token.END_ARRAY) { // skip } else if (this.parser.currentToken() == Token.START_OBJECT) { - parseToken(path, currentFieldName); - int dotIndex = path.lastIndexOf(DOT_SYMBOL, path.length()); + parseToken(path, currentFieldName, true); - if (dotIndex != -1 && path.length() > currentFieldName.length()) { - path.setLength(path.length() - currentFieldName.length() - 1); + if (popName) { + int dotIndex = path.lastIndexOf(DOT_SYMBOL); + if (dotIndex != -1) { + path.setLength(path.length() - currentFieldName.length() - 1); + } } } else { if (!path.toString().contains(currentFieldName)) { @@ -118,12 +119,15 @@ private void parseToken(StringBuilder path, String currentFieldName) throws IOEx parseValue(parsedFields); this.valueList.add(parsedFields.toString()); this.valueAndPathList.add(path + EQUAL_SYMBOL + parsedFields); - int dotIndex = path.lastIndexOf(DOT_SYMBOL, path.length()); - if (dotIndex != -1 && path.length() > currentFieldName.length()) { - path.setLength(path.length() - currentFieldName.length() - 1); + + if (popName) { + int dotIndex = path.lastIndexOf(DOT_SYMBOL); + + if (dotIndex != -1) { + path.setLength(path.length() - currentFieldName.length() - 1); + } } } - } } diff --git a/server/src/test/java/org/opensearch/common/xcontent/JsonToStringXContentParserTests.java b/server/src/test/java/org/opensearch/common/xcontent/JsonToStringXContentParserTests.java index 0feb7bcd1ceec..479c62be0abbd 100644 --- a/server/src/test/java/org/opensearch/common/xcontent/JsonToStringXContentParserTests.java +++ b/server/src/test/java/org/opensearch/common/xcontent/JsonToStringXContentParserTests.java @@ -110,4 +110,25 @@ public void testNestChildObjectWithDotsAndFieldWithDots() throws IOException { ); } + public void testArrayOfObjects() throws IOException { + String jsonExample ="{" + + "\"field\": {" + + " \"detail\": {" + + " \"foooooooooooo\": [" + + " {\"name\":\"baz\"}," + + " {\"name\":\"baz\"}" + + " ]" + + " }" + + "}}"; + + assertEquals("{" + + "\"flat\":[\"field\",\"detail\",\"foooooooooooo\",\"name\",\"name\"]," + + "\"flat._value\":[\"baz\",\"baz\"]," + + "\"flat._valueAndPath\":[" + + "\"flat.field.detail.foooooooooooo.name=baz\"," + + "\"flat.field.detail.foooooooooooo.name=baz\"" + + "]}", + flattenJsonString("flat", jsonExample) + ); + } } From 4f3be3538196dac93f7f815b93f94a2f326bbfc3 Mon Sep 17 00:00:00 2001 From: naomichi-y Date: Fri, 10 May 2024 13:43:48 +0900 Subject: [PATCH 2/6] Add fix description Signed-off-by: naomichi-y --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 34cd4c2097e48..cf0d032cc75ea 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -46,6 +46,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), - Fix missing value of FieldSort for unsigned_long ([#14963](https://github.com/opensearch-project/OpenSearch/pull/14963)) - Fix delete index template failed when the index template matches a data stream but is unused ([#15080](https://github.com/opensearch-project/OpenSearch/pull/15080)) - Fix array_index_out_of_bounds_exception when indexing documents with field name containing only dot ([#15126](https://github.com/opensearch-project/OpenSearch/pull/15126)) +- Fixed array field name omission in flat_object function for nested JSON ([#13620](https://github.com/opensearch-project/OpenSearch/pull/13620)) ### Security From ca4f31e5a8446cbc46a0bc436ee49705caa3600b Mon Sep 17 00:00:00 2001 From: Michael Froh Date: Fri, 9 Aug 2024 14:17:15 -0700 Subject: [PATCH 3/6] Refactor JsonToStringXContentParser#parseToken This method was pretty complicated, hard to follow, and therefore prone to bugs. This change introduces a proper stack (rather than a StringBuilder) to keep track of fields under fields. When ever we parse a field name, we push it onto the stack, recursively parse its value (an object, an array, or a primitive value), then we pop (guaranteeing pushes and pops are balanced). Signed-off-by: Michael Froh --- .../xcontent/JsonToStringXContentParser.java | 105 +++++++----------- .../JsonToStringXContentParserTests.java | 66 ++++++++--- 2 files changed, 91 insertions(+), 80 deletions(-) diff --git a/server/src/main/java/org/opensearch/common/xcontent/JsonToStringXContentParser.java b/server/src/main/java/org/opensearch/common/xcontent/JsonToStringXContentParser.java index 1869c809400ce..fc89b3c5fc058 100644 --- a/server/src/main/java/org/opensearch/common/xcontent/JsonToStringXContentParser.java +++ b/server/src/main/java/org/opensearch/common/xcontent/JsonToStringXContentParser.java @@ -8,6 +8,7 @@ package org.opensearch.common.xcontent; +import org.apache.logging.log4j.util.Strings; import org.opensearch.common.xcontent.json.JsonXContent; import org.opensearch.core.common.bytes.BytesReference; import org.opensearch.core.xcontent.AbstractXContentParser; @@ -23,6 +24,9 @@ import java.math.BigInteger; import java.nio.CharBuffer; import java.util.ArrayList; +import java.util.Collections; +import java.util.Deque; +import java.util.LinkedList; /** * JsonToStringParser is the main parser class to transform JSON into stringFields in a XContentParser @@ -46,7 +50,6 @@ public class JsonToStringXContentParser extends AbstractXContentParser { private static final String VALUE_AND_PATH_SUFFIX = "._valueAndPath"; private static final String VALUE_SUFFIX = "._value"; - private static final String DOT_SYMBOL = "."; private static final String EQUAL_SYMBOL = "="; public JsonToStringXContentParser( @@ -65,7 +68,7 @@ public JsonToStringXContentParser( public XContentParser parseObject() throws IOException { builder.startObject(); StringBuilder path = new StringBuilder(fieldTypeName); - parseToken(path, null, true); + parseToken(new LinkedList<>(Collections.singleton(fieldTypeName))); builder.field(this.fieldTypeName, keyList); builder.field(this.fieldTypeName + VALUE_SUFFIX, valueList); builder.field(this.fieldTypeName + VALUE_AND_PATH_SUFFIX, valueAndPathList); @@ -74,79 +77,55 @@ public XContentParser parseObject() throws IOException { return JsonXContent.jsonXContent.createParser(this.xContentRegistry, this.deprecationHandler, String.valueOf(jString)); } - private void parseToken(StringBuilder path, String currentFieldName, boolean popName) throws IOException { - while (this.parser.nextToken() != Token.END_OBJECT) { - if (this.parser.currentName() != null) { - currentFieldName = this.parser.currentName(); + private void parseToken(Deque path) throws IOException { + if (this.parser.currentToken() == Token.FIELD_NAME) { + String fieldName = this.parser.currentName(); + path.addLast(fieldName); // Pushing onto the stack *must* be matched by pop + String parts = fieldName; + while (parts.contains(".")) { // Extract the intermediate keys maybe present in fieldName + int dotPos = parts.indexOf('.'); + String part = parts.substring(0, dotPos); + this.keyList.add(part); + parts = parts.substring(dotPos + 1); } - StringBuilder parsedFields = new StringBuilder(); - - if (this.parser.currentToken() == Token.FIELD_NAME) { - path.append(DOT_SYMBOL).append(currentFieldName); - int dotIndex = currentFieldName.indexOf(DOT_SYMBOL); - String fieldNameSuffix = currentFieldName; - // The field name may be of the form foo.bar.baz - // If that's the case, each "part" is a key. - while (dotIndex >= 0) { - String fieldNamePrefix = fieldNameSuffix.substring(0, dotIndex); - if (!fieldNamePrefix.isEmpty()) { - this.keyList.add(fieldNamePrefix); - } - fieldNameSuffix = fieldNameSuffix.substring(dotIndex + 1); - dotIndex = fieldNameSuffix.indexOf(DOT_SYMBOL); - } - if (!fieldNameSuffix.isEmpty()) { - this.keyList.add(fieldNameSuffix); - } - } else if (this.parser.currentToken() == Token.START_ARRAY) { - parseToken(path, currentFieldName, false); - break; - } else if (this.parser.currentToken() == Token.END_ARRAY) { - // skip - } else if (this.parser.currentToken() == Token.START_OBJECT) { - parseToken(path, currentFieldName, true); - - if (popName) { - int dotIndex = path.lastIndexOf(DOT_SYMBOL); - if (dotIndex != -1) { - path.setLength(path.length() - currentFieldName.length() - 1); - } - } - } else { - if (!path.toString().contains(currentFieldName)) { - path.append(DOT_SYMBOL).append(currentFieldName); - } - parseValue(parsedFields); - this.valueList.add(parsedFields.toString()); - this.valueAndPathList.add(path + EQUAL_SYMBOL + parsedFields); - - if (popName) { - int dotIndex = path.lastIndexOf(DOT_SYMBOL); - - if (dotIndex != -1) { - path.setLength(path.length() - currentFieldName.length() - 1); - } - } + this.keyList.add(parts); // parts has no dot, so either it's the original fieldName or it's the last part + this.parser.nextToken(); // advance to the value of fieldName + parseToken(path); // parse the value for fieldName (which will be an array, an object, or a primitive value) + path.removeLast(); // Here is where we pop fieldName from the stack (since we're done with the value of fieldName) + // Note that whichever other branch we just passed through has already ended with nextToken(), so we + // don't need ot call it. + } else if (this.parser.currentToken() == Token.START_ARRAY) { + parser.nextToken(); + while (this.parser.currentToken() != Token.END_ARRAY) { + parseToken(path); } + this.parser.nextToken(); + } else if (this.parser.currentToken() == Token.START_OBJECT) { + parser.nextToken(); + while (this.parser.currentToken() != Token.END_OBJECT) { + parseToken(path); + } + this.parser.nextToken(); + } else if (this.parser.currentToken().isValue()) { + String parsedValue = parseValue(); + if (parsedValue != null) { + this.valueList.add(parsedValue); + this.valueAndPathList.add(Strings.join(path, '.') + EQUAL_SYMBOL + parsedValue); + } + this.parser.nextToken(); } } - private void parseValue(StringBuilder parsedFields) throws IOException { + private String parseValue() throws IOException { switch (this.parser.currentToken()) { case VALUE_BOOLEAN: case VALUE_NUMBER: case VALUE_STRING: case VALUE_NULL: - parsedFields.append(this.parser.textOrNull()); - break; + return this.parser.textOrNull(); // Handle other token types as needed - case FIELD_NAME: - case VALUE_EMBEDDED_OBJECT: - case END_ARRAY: - case START_ARRAY: - break; default: - throw new IOException("Unsupported token type [" + parser.currentToken() + "]"); + throw new IOException("Unsupported value token type [" + parser.currentToken() + "]"); } } diff --git a/server/src/test/java/org/opensearch/common/xcontent/JsonToStringXContentParserTests.java b/server/src/test/java/org/opensearch/common/xcontent/JsonToStringXContentParserTests.java index 479c62be0abbd..4670c14144eee 100644 --- a/server/src/test/java/org/opensearch/common/xcontent/JsonToStringXContentParserTests.java +++ b/server/src/test/java/org/opensearch/common/xcontent/JsonToStringXContentParserTests.java @@ -111,23 +111,55 @@ public void testNestChildObjectWithDotsAndFieldWithDots() throws IOException { } public void testArrayOfObjects() throws IOException { - String jsonExample ="{" + - "\"field\": {" + - " \"detail\": {" + - " \"foooooooooooo\": [" + - " {\"name\":\"baz\"}," + - " {\"name\":\"baz\"}" + - " ]" + - " }" + - "}}"; - - assertEquals("{" + - "\"flat\":[\"field\",\"detail\",\"foooooooooooo\",\"name\",\"name\"]," + - "\"flat._value\":[\"baz\",\"baz\"]," + - "\"flat._valueAndPath\":[" + - "\"flat.field.detail.foooooooooooo.name=baz\"," + - "\"flat.field.detail.foooooooooooo.name=baz\"" + - "]}", + String jsonExample = "{" + + "\"field\": {" + + " \"detail\": {" + + " \"foooooooooooo\": [" + + " {\"name\":\"baz\"}," + + " {\"name\":\"baz\"}" + + " ]" + + " }" + + "}}"; + + assertEquals( + "{" + + "\"flat\":[\"field\",\"detail\",\"foooooooooooo\",\"name\",\"name\"]," + + "\"flat._value\":[\"baz\",\"baz\"]," + + "\"flat._valueAndPath\":[" + + "\"flat.field.detail.foooooooooooo.name=baz\"," + + "\"flat.field.detail.foooooooooooo.name=baz\"" + + "]}", + flattenJsonString("flat", jsonExample) + ); + } + + public void testArraysOfObjectsAndValues() throws IOException { + String jsonExample = "{" + + "\"field\": {" + + " \"detail\": {" + + " \"foooooooooooo\": [" + + " {\"name\":\"baz\"}," + + " {\"name\":\"baz\"}" + + " ]" + + " }," + + " \"numbers\" : [" + + " 1," + + " 2," + + " 3" + + " ]" + + "}}"; + + assertEquals( + "{" + + "\"flat\":[\"field\",\"detail\",\"foooooooooooo\",\"name\",\"name\",\"numbers\"]," + + "\"flat._value\":[\"baz\",\"baz\",\"1\",\"2\",\"3\"]," + + "\"flat._valueAndPath\":[" + + "\"flat.field.detail.foooooooooooo.name=baz\"," + + "\"flat.field.detail.foooooooooooo.name=baz\"," + + "\"flat.field.numbers=1\"," + + "\"flat.field.numbers=2\"," + + "\"flat.field.numbers=3\"" + + "]}", flattenJsonString("flat", jsonExample) ); } From 7f176b377034b068435218ee14143594a151b710 Mon Sep 17 00:00:00 2001 From: Michael Froh Date: Mon, 12 Aug 2024 13:41:51 -0700 Subject: [PATCH 4/6] Remove use of org.apache.logging.log4j.util.Strings Signed-off-by: Michael Froh --- .../common/xcontent/JsonToStringXContentParser.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/server/src/main/java/org/opensearch/common/xcontent/JsonToStringXContentParser.java b/server/src/main/java/org/opensearch/common/xcontent/JsonToStringXContentParser.java index fc89b3c5fc058..d02e1e6d811aa 100644 --- a/server/src/main/java/org/opensearch/common/xcontent/JsonToStringXContentParser.java +++ b/server/src/main/java/org/opensearch/common/xcontent/JsonToStringXContentParser.java @@ -8,8 +8,8 @@ package org.opensearch.common.xcontent; -import org.apache.logging.log4j.util.Strings; import org.opensearch.common.xcontent.json.JsonXContent; +import org.opensearch.core.common.Strings; import org.opensearch.core.common.bytes.BytesReference; import org.opensearch.core.xcontent.AbstractXContentParser; import org.opensearch.core.xcontent.DeprecationHandler; @@ -110,7 +110,7 @@ private void parseToken(Deque path) throws IOException { String parsedValue = parseValue(); if (parsedValue != null) { this.valueList.add(parsedValue); - this.valueAndPathList.add(Strings.join(path, '.') + EQUAL_SYMBOL + parsedValue); + this.valueAndPathList.add(Strings.collectionToDelimitedString(path, ".") + EQUAL_SYMBOL + parsedValue); } this.parser.nextToken(); } From 02c12c6bd714b3804de3dab4f3e3b230839f94d4 Mon Sep 17 00:00:00 2001 From: Michael Froh Date: Tue, 13 Aug 2024 10:54:07 -0700 Subject: [PATCH 5/6] Make sure JsonToStringXContentParser ends on END_OBJECT There is logic in DocumentParser that expects any object parser to start with currentToken() pointing to START_OBJECT, and return with currentToken() pointing to END_OBJECT. My previous commits were stepping over the start/end, and returning on the token following the end. Signed-off-by: Michael Froh --- .../xcontent/JsonToStringXContentParser.java | 25 +++++++++++-------- .../index/mapper/FlatObjectFieldMapper.java | 4 +-- .../JsonToStringXContentParserTests.java | 4 +-- 3 files changed, 19 insertions(+), 14 deletions(-) diff --git a/server/src/main/java/org/opensearch/common/xcontent/JsonToStringXContentParser.java b/server/src/main/java/org/opensearch/common/xcontent/JsonToStringXContentParser.java index d02e1e6d811aa..d24571fc5778d 100644 --- a/server/src/main/java/org/opensearch/common/xcontent/JsonToStringXContentParser.java +++ b/server/src/main/java/org/opensearch/common/xcontent/JsonToStringXContentParser.java @@ -36,17 +36,17 @@ */ public class JsonToStringXContentParser extends AbstractXContentParser { private final String fieldTypeName; - private XContentParser parser; + private final XContentParser parser; - private ArrayList valueList = new ArrayList<>(); - private ArrayList valueAndPathList = new ArrayList<>(); - private ArrayList keyList = new ArrayList<>(); + private final ArrayList valueList = new ArrayList<>(); + private final ArrayList valueAndPathList = new ArrayList<>(); + private final ArrayList keyList = new ArrayList<>(); - private XContentBuilder builder = XContentBuilder.builder(JsonXContent.jsonXContent); + private final XContentBuilder builder = XContentBuilder.builder(JsonXContent.jsonXContent); - private NamedXContentRegistry xContentRegistry; + private final NamedXContentRegistry xContentRegistry; - private DeprecationHandler deprecationHandler; + private final DeprecationHandler deprecationHandler; private static final String VALUE_AND_PATH_SUFFIX = "._valueAndPath"; private static final String VALUE_SUFFIX = "._value"; @@ -66,9 +66,14 @@ public JsonToStringXContentParser( } public XContentParser parseObject() throws IOException { + assert currentToken() == Token.START_OBJECT; + parser.nextToken(); // Skip the outer START_OBJECT. Need to return on END_OBJECT. + builder.startObject(); - StringBuilder path = new StringBuilder(fieldTypeName); - parseToken(new LinkedList<>(Collections.singleton(fieldTypeName))); + LinkedList path = new LinkedList<>(Collections.singleton(fieldTypeName)); + while (currentToken() != Token.END_OBJECT) { + parseToken(path); + } builder.field(this.fieldTypeName, keyList); builder.field(this.fieldTypeName + VALUE_SUFFIX, valueList); builder.field(this.fieldTypeName + VALUE_AND_PATH_SUFFIX, valueAndPathList); @@ -93,7 +98,7 @@ private void parseToken(Deque path) throws IOException { parseToken(path); // parse the value for fieldName (which will be an array, an object, or a primitive value) path.removeLast(); // Here is where we pop fieldName from the stack (since we're done with the value of fieldName) // Note that whichever other branch we just passed through has already ended with nextToken(), so we - // don't need ot call it. + // don't need to call it. } else if (this.parser.currentToken() == Token.START_ARRAY) { parser.nextToken(); while (this.parser.currentToken() != Token.END_ARRAY) { diff --git a/server/src/main/java/org/opensearch/index/mapper/FlatObjectFieldMapper.java b/server/src/main/java/org/opensearch/index/mapper/FlatObjectFieldMapper.java index 9a3f2595a7c9e..c1a2ef4804c1b 100644 --- a/server/src/main/java/org/opensearch/index/mapper/FlatObjectFieldMapper.java +++ b/server/src/main/java/org/opensearch/index/mapper/FlatObjectFieldMapper.java @@ -569,7 +569,7 @@ protected void parseCreateField(ParseContext context) throws IOException { String value = context.externalValue().toString(); parseValueAddFields(context, value, fieldType().name()); } else { - JsonToStringXContentParser JsonToStringParser = new JsonToStringXContentParser( + JsonToStringXContentParser jsonToStringParser = new JsonToStringXContentParser( NamedXContentRegistry.EMPTY, DeprecationHandler.IGNORE_DEPRECATIONS, context.parser(), @@ -579,7 +579,7 @@ protected void parseCreateField(ParseContext context) throws IOException { JsonToStringParser is the main parser class to transform JSON into stringFields in a XContentParser It reads the JSON object and parsed to a list of string */ - XContentParser parser = JsonToStringParser.parseObject(); + XContentParser parser = jsonToStringParser.parseObject(); XContentParser.Token currentToken; while ((currentToken = parser.nextToken()) != XContentParser.Token.END_OBJECT) { diff --git a/server/src/test/java/org/opensearch/common/xcontent/JsonToStringXContentParserTests.java b/server/src/test/java/org/opensearch/common/xcontent/JsonToStringXContentParserTests.java index 4670c14144eee..a0f5150981a08 100644 --- a/server/src/test/java/org/opensearch/common/xcontent/JsonToStringXContentParserTests.java +++ b/server/src/test/java/org/opensearch/common/xcontent/JsonToStringXContentParserTests.java @@ -19,7 +19,6 @@ public class JsonToStringXContentParserTests extends OpenSearchTestCase { private String flattenJsonString(String fieldName, String in) throws IOException { - String transformed; try ( XContentParser parser = JsonXContent.jsonXContent.createParser( xContentRegistry(), @@ -33,10 +32,11 @@ private String flattenJsonString(String fieldName, String in) throws IOException parser, fieldName ); - // Skip the START_OBJECT token: + // Point to the first token (should be START_OBJECT) jsonToStringXContentParser.nextToken(); XContentParser transformedParser = jsonToStringXContentParser.parseObject(); + assertSame(XContentParser.Token.END_OBJECT, jsonToStringXContentParser.currentToken()); try (XContentBuilder jsonBuilder = XContentFactory.jsonBuilder()) { jsonBuilder.copyCurrentStructure(transformedParser); return jsonBuilder.toString(); From cac0d94ec7db8908b8f95d7d318e1f4d86e96c91 Mon Sep 17 00:00:00 2001 From: Michael Froh Date: Tue, 13 Aug 2024 12:54:21 -0700 Subject: [PATCH 6/6] Throw exception when flat object is null The existing behavior throws an exception on null flat object by advancing the parser beyond the end of the flat object input. We could simply skip a null flat_object field, but this would be a behavioral change from 2.7. Signed-off-by: Michael Froh --- .../org/opensearch/index/mapper/FlatObjectFieldMapper.java | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/server/src/main/java/org/opensearch/index/mapper/FlatObjectFieldMapper.java b/server/src/main/java/org/opensearch/index/mapper/FlatObjectFieldMapper.java index c1a2ef4804c1b..b82fa3999612a 100644 --- a/server/src/main/java/org/opensearch/index/mapper/FlatObjectFieldMapper.java +++ b/server/src/main/java/org/opensearch/index/mapper/FlatObjectFieldMapper.java @@ -568,6 +568,10 @@ protected void parseCreateField(ParseContext context) throws IOException { if (context.externalValueSet()) { String value = context.externalValue().toString(); parseValueAddFields(context, value, fieldType().name()); + } else if (context.parser().currentToken() == XContentParser.Token.VALUE_NULL) { + context.parser().nextToken(); // This triggers an exception in DocumentParser. + // We could remove the above nextToken() call to skip the null value, but the existing + // behavior (since 2.7) throws the exception. } else { JsonToStringXContentParser jsonToStringParser = new JsonToStringXContentParser( NamedXContentRegistry.EMPTY, @@ -594,7 +598,6 @@ protected void parseCreateField(ParseContext context) throws IOException { } } - } }