Skip to content

Commit 053e154

Browse files
authored
Add option to ObjectParser to consume unknown fields (#42491)
ObjectParser has two ways of dealing with unknown fields: ignore them entirely, or throw an error. Sometimes it can be useful instead to gather up these unknown fields and record them separately, for example as arbitrary entries in a map. This commit adds the ability to specify an unknown field consumer on an ObjectParser, called with the field name and parsed value of each unknown field encountered during parsing. The public API of ObjectParser is largely unchanged, with a single new constructor method and interface definition.
1 parent be8020a commit 053e154

File tree

2 files changed

+121
-21
lines changed

2 files changed

+121
-21
lines changed

libs/x-content/src/main/java/org/elasticsearch/common/xcontent/ObjectParser.java

+82-21
Original file line numberDiff line numberDiff line change
@@ -78,14 +78,63 @@ public static <Value, ElementValue> BiConsumer<Value, List<ElementValue>> fromLi
7878
};
7979
}
8080

81+
private interface UnknownFieldParser<Value, Context> {
82+
83+
void acceptUnknownField(String parserName, String field, XContentLocation location, XContentParser parser,
84+
Value value, Context context) throws IOException;
85+
}
86+
87+
private static <Value, Context> UnknownFieldParser<Value, Context> ignoreUnknown() {
88+
return (n, f, l, p, v, c) -> p.skipChildren();
89+
}
90+
91+
private static <Value, Context> UnknownFieldParser<Value, Context> errorOnUnknown() {
92+
return (n, f, l, p, v, c) -> {
93+
throw new XContentParseException(l, "[" + n + "] unknown field [" + f + "], parser not found");
94+
};
95+
}
96+
97+
/**
98+
* Defines how to consume a parsed undefined field
99+
*/
100+
public interface UnknownFieldConsumer<Value> {
101+
void accept(Value target, String field, Object value);
102+
}
103+
104+
private static <Value, Context> UnknownFieldParser<Value, Context> consumeUnknownField(UnknownFieldConsumer<Value> consumer) {
105+
return (parserName, field, location, parser, value, context) -> {
106+
XContentParser.Token t = parser.currentToken();
107+
switch (t) {
108+
case VALUE_STRING:
109+
consumer.accept(value, field, parser.text());
110+
break;
111+
case VALUE_NUMBER:
112+
consumer.accept(value, field, parser.numberValue());
113+
break;
114+
case VALUE_BOOLEAN:
115+
consumer.accept(value, field, parser.booleanValue());
116+
break;
117+
case VALUE_NULL:
118+
consumer.accept(value, field, null);
119+
break;
120+
case START_OBJECT:
121+
consumer.accept(value, field, parser.map());
122+
break;
123+
case START_ARRAY:
124+
consumer.accept(value, field, parser.list());
125+
break;
126+
default:
127+
throw new XContentParseException(parser.getTokenLocation(),
128+
"[" + parserName + "] cannot parse field [" + field + "] with value type [" + t + "]");
129+
}
130+
};
131+
}
132+
81133
private final Map<String, FieldParser> fieldParserMap = new HashMap<>();
82134
private final String name;
83135
private final Supplier<Value> valueSupplier;
84-
/**
85-
* Should this parser ignore unknown fields? This should generally be set to true only when parsing responses from external systems,
86-
* never when parsing requests from users.
87-
*/
88-
private final boolean ignoreUnknownFields;
136+
137+
private final UnknownFieldParser<Value, Context> unknownFieldParser;
89138

90139
/**
91140
* Creates a new ObjectParser instance with a name. This name is used to reference the parser in exceptions and messages.
@@ -95,25 +144,45 @@ public ObjectParser(String name) {
95144
}
96145

97146
/**
98-
* Creates a new ObjectParser instance which a name.
147+
* Creates a new ObjectParser instance with a name.
99148
* @param name the parsers name, used to reference the parser in exceptions and messages.
100149
* @param valueSupplier a supplier that creates a new Value instance used when the parser is used as an inner object parser.
101150
*/
102151
public ObjectParser(String name, @Nullable Supplier<Value> valueSupplier) {
103-
this(name, false, valueSupplier);
152+
this(name, errorOnUnknown(), valueSupplier);
104153
}
105154

106155
/**
107-
* Creates a new ObjectParser instance which a name.
156+
* Creates a new ObjectParser instance with a name.
108157
* @param name the parsers name, used to reference the parser in exceptions and messages.
109158
* @param ignoreUnknownFields Should this parser ignore unknown fields? This should generally be set to true only when parsing
110159
* responses from external systems, never when parsing requests from users.
111160
* @param valueSupplier a supplier that creates a new Value instance used when the parser is used as an inner object parser.
112161
*/
113162
public ObjectParser(String name, boolean ignoreUnknownFields, @Nullable Supplier<Value> valueSupplier) {
163+
this(name, ignoreUnknownFields ? ignoreUnknown() : errorOnUnknown(), valueSupplier);
164+
}
165+
166+
/**
167+
* Creates a new ObjectParser instance with a name.
168+
* @param name the parsers name, used to reference the parser in exceptions and messages.
169+
* @param unknownFieldConsumer how to consume parsed unknown fields
170+
* @param valueSupplier a supplier that creates a new Value instance used when the parser is used as an inner object parser.
171+
*/
172+
public ObjectParser(String name, UnknownFieldConsumer<Value> unknownFieldConsumer, @Nullable Supplier<Value> valueSupplier) {
173+
this(name, consumeUnknownField(unknownFieldConsumer), valueSupplier);
174+
}
175+
176+
/**
177+
* Creates a new ObjectParser instance with a name.
178+
* @param name the parsers name, used to reference the parser in exceptions and messages.
179+
* @param unknownFieldParser how to parse unknown fields
180+
* @param valueSupplier a supplier that creates a new Value instance used when the parser is used as an inner object parser.
181+
*/
182+
private ObjectParser(String name, UnknownFieldParser<Value, Context> unknownFieldParser, @Nullable Supplier<Value> valueSupplier) {
114183
this.name = name;
115184
this.valueSupplier = valueSupplier;
116-
this.ignoreUnknownFields = ignoreUnknownFields;
185+
this.unknownFieldParser = unknownFieldParser;
117186
}
118187

119188
/**
@@ -152,17 +221,18 @@ public Value parse(XContentParser parser, Value value, Context context) throws I
152221

153222
FieldParser fieldParser = null;
154223
String currentFieldName = null;
224+
XContentLocation currentPosition = null;
155225
while ((token = parser.nextToken()) != XContentParser.Token.END_OBJECT) {
156226
if (token == XContentParser.Token.FIELD_NAME) {
157227
currentFieldName = parser.currentName();
158-
fieldParser = getParser(currentFieldName, parser);
228+
currentPosition = parser.getTokenLocation();
229+
fieldParser = fieldParserMap.get(currentFieldName);
159230
} else {
160231
if (currentFieldName == null) {
161232
throw new XContentParseException(parser.getTokenLocation(), "[" + name + "] no field found");
162233
}
163234
if (fieldParser == null) {
164-
assert ignoreUnknownFields : "this should only be possible if configured to ignore known fields";
165-
parser.skipChildren(); // noop if parser points to a value, skips children if parser is start object or start array
235+
unknownFieldParser.acceptUnknownField(name, currentFieldName, currentPosition, parser, value, context);
166236
} else {
167237
fieldParser.assertSupports(name, parser, currentFieldName);
168238
parseSub(parser, fieldParser, currentFieldName, value, context);
@@ -363,15 +433,6 @@ private void parseSub(XContentParser parser, FieldParser fieldParser, String cur
363433
}
364434
}
365435

366-
private FieldParser getParser(String fieldName, XContentParser xContentParser) {
367-
FieldParser parser = fieldParserMap.get(fieldName);
368-
if (parser == null && false == ignoreUnknownFields) {
369-
throw new XContentParseException(xContentParser.getTokenLocation(),
370-
"[" + name + "] unknown field [" + fieldName + "], parser not found");
371-
}
372-
return parser;
373-
}
374-
375436
private class FieldParser {
376437
private final Parser<Value, Context> parser;
377438
private final EnumSet<XContentParser.Token> supportedTokens;

libs/x-content/src/test/java/org/elasticsearch/common/xcontent/ObjectParserTests.java

+39
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,9 @@
3333
import java.util.ArrayList;
3434
import java.util.Arrays;
3535
import java.util.Collections;
36+
import java.util.HashMap;
3637
import java.util.List;
38+
import java.util.Map;
3739
import java.util.concurrent.atomic.AtomicReference;
3840

3941
import static org.hamcrest.Matchers.containsString;
@@ -733,4 +735,41 @@ public void setFoo(int foo) {
733735
this.foo = foo;
734736
}
735737
}
738+
739+
private static class ObjectWithArbitraryFields {
740+
String name;
741+
Map<String, Object> fields = new HashMap<>();
742+
void setField(String key, Object value) {
743+
fields.put(key, value);
744+
}
745+
void setName(String name) {
746+
this.name = name;
747+
}
748+
}
749+
750+
public void testConsumeUnknownFields() throws IOException {
751+
XContentParser parser = createParser(JsonXContent.jsonXContent,
752+
"{\n"
753+
+ " \"test\" : \"foo\",\n"
754+
+ " \"test_number\" : 2,\n"
755+
+ " \"name\" : \"geoff\",\n"
756+
+ " \"test_boolean\" : true,\n"
757+
+ " \"test_null\" : null,\n"
758+
+ " \"test_array\": [1,2,3,4],\n"
759+
+ " \"test_nested\": { \"field\" : \"value\", \"field2\" : [ \"list1\", \"list2\" ] }\n"
760+
+ "}");
761+
ObjectParser<ObjectWithArbitraryFields, Void> op
762+
= new ObjectParser<>("unknown", ObjectWithArbitraryFields::setField, ObjectWithArbitraryFields::new);
763+
op.declareString(ObjectWithArbitraryFields::setName, new ParseField("name"));
764+
765+
ObjectWithArbitraryFields o = op.parse(parser, null);
766+
assertEquals("geoff", o.name);
767+
assertEquals(6, o.fields.size());
768+
assertEquals("foo", o.fields.get("test"));
769+
assertEquals(2, o.fields.get("test_number"));
770+
assertEquals(true, o.fields.get("test_boolean"));
771+
assertNull(o.fields.get("test_null"));
772+
assertEquals(List.of(1, 2, 3, 4), o.fields.get("test_array"));
773+
assertEquals(Map.of("field", "value", "field2", List.of("list1", "list2")), o.fields.get("test_nested"));
774+
}
736775
}

0 commit comments

Comments
 (0)