Ver código fonte

Refactor ObjectParser and CompatibleObjectParser to support REST Compatible API (#68808)

In order to support compatible fields when parsing XContent additional information has to be set during ParsedField declaration.
This commit adds a set of RestApiCompatibleVersion on a ParsedField in order to specify on which versions a field is supported. By default ParsedField is allowed to be parsed on both current and previous major versions.

ObjectParser - which is used for constructing objects using 'setters' - has a modified fieldParsersMap to be Map of Maps. with key being RestApiCompatibility. This allows to choose set of field-parsers as specified on a request.
Under RestApiCompatibility.minimumSupported key, there is a map that contains field-parsers for both previous and current versions.
Under RestApiCompatibility.current there will be only current versions field (compatible fields not a present)

ConstructingObjectParser - which is used for constructing objects using 'constructors' - is modified to contain a map of Version To constructorArgInfo , declarations of fields to be set on a constructor depending on a version

relates #51816
Przemyslaw Gomulka 4 anos atrás
pai
commit
f22adc47d8

+ 2 - 2
libs/core/src/main/java/org/elasticsearch/common/compatibility/RestApiCompatibleVersion.java

@@ -19,8 +19,8 @@ public enum RestApiCompatibleVersion {
     V_8(8),
     V_7(7);
 
-    public byte major;
-    private static RestApiCompatibleVersion CURRENT = V_8;
+    public final byte major;
+    private static final RestApiCompatibleVersion CURRENT = V_8;
 
     RestApiCompatibleVersion(int major) {
         this.major = (byte) major;

+ 37 - 9
libs/x-content/src/main/java/org/elasticsearch/common/ParseField.java

@@ -7,11 +7,15 @@
  */
 package org.elasticsearch.common;
 
+import org.elasticsearch.common.compatibility.RestApiCompatibleVersion;
 import org.elasticsearch.common.xcontent.DeprecationHandler;
 import org.elasticsearch.common.xcontent.XContentLocation;
 
+import java.util.Arrays;
+import java.util.Collection;
 import java.util.Collections;
 import java.util.HashSet;
+import java.util.List;
 import java.util.Objects;
 import java.util.Set;
 import java.util.function.Supplier;
@@ -23,21 +27,15 @@ import java.util.function.Supplier;
 public class ParseField {
     private final String name;
     private final String[] deprecatedNames;
+    private final Set<RestApiCompatibleVersion> restApiCompatibleVersions = new HashSet<>(2);
     private String allReplacedWith = null;
     private final String[] allNames;
     private boolean fullyDeprecated = false;
 
     private static final String[] EMPTY = new String[0];
 
-    /**
-     * @param name
-     *            the primary name for this field. This will be returned by
-     *            {@link #getPreferredName()}
-     * @param deprecatedNames
-     *            names for this field which are deprecated and will not be
-     *            accepted when strict matching is used.
-     */
-    public ParseField(String name, String... deprecatedNames) {
+
+    private ParseField(String name, Collection<RestApiCompatibleVersion> restApiCompatibleVersions, String[] deprecatedNames) {
         this.name = name;
         if (deprecatedNames == null || deprecatedNames.length == 0) {
             this.deprecatedNames = EMPTY;
@@ -46,12 +44,25 @@ public class ParseField {
             Collections.addAll(set, deprecatedNames);
             this.deprecatedNames = set.toArray(new String[set.size()]);
         }
+        this.restApiCompatibleVersions.addAll(restApiCompatibleVersions);
+
         Set<String> allNames = new HashSet<>();
         allNames.add(name);
         Collections.addAll(allNames, this.deprecatedNames);
         this.allNames = allNames.toArray(new String[allNames.size()]);
     }
 
+    /**
+     * Creates a field available for lookup for both current and previous REST API compatible versions
+     * @param name            the primary name for this field. This will be returned by
+     *                        {@link #getPreferredName()}
+     * @param deprecatedNames names for this field which are deprecated and will not be
+     *                        accepted when strict matching is used.
+     */
+    public ParseField(String name, String... deprecatedNames) {
+        this(name, List.of(RestApiCompatibleVersion.currentVersion(), RestApiCompatibleVersion.minimumSupported()) ,deprecatedNames);
+    }
+
     /**
      * @return the preferred name used for this field
      */
@@ -78,6 +89,22 @@ public class ParseField {
         return new ParseField(this.name, deprecatedNames);
     }
 
+
+    /**
+     * Creates a new field with current name and deprecatedNames, but overrides restApiCompatibleVersions
+     * @param restApiCompatibleVersions rest api compatibility versions under which specifies when a lookup will be allowed
+     */
+    public ParseField withRestApiCompatibilityVersions(RestApiCompatibleVersion... restApiCompatibleVersions) {
+        return new ParseField(this.name, Arrays.asList(restApiCompatibleVersions), this.deprecatedNames);
+    }
+
+    /**
+     * @return rest api compatibility versions under which a lookup will be allowed
+     */
+    public Set<RestApiCompatibleVersion> getRestApiCompatibleVersions() {
+        return restApiCompatibleVersions;
+    }
+
     /**
      * Return a new ParseField where all field names are deprecated and replaced
      * with {@code allReplacedWith}.
@@ -169,6 +196,7 @@ public class ParseField {
         return deprecatedNames;
     }
 
+
     public static class CommonFields {
         public static final ParseField FIELD = new ParseField("field");
         public static final ParseField FIELDS = new ParseField("fields");

+ 38 - 20
libs/x-content/src/main/java/org/elasticsearch/common/xcontent/ConstructingObjectParser.java

@@ -9,16 +9,21 @@
 package org.elasticsearch.common.xcontent;
 
 import org.elasticsearch.common.ParseField;
+import org.elasticsearch.common.compatibility.RestApiCompatibleVersion;
 import org.elasticsearch.common.xcontent.ObjectParser.NamedObjectParser;
 import org.elasticsearch.common.xcontent.ObjectParser.ValueType;
 
 import java.io.IOException;
 import java.util.ArrayList;
+import java.util.Collections;
+import java.util.EnumMap;
 import java.util.List;
+import java.util.Map;
 import java.util.function.BiConsumer;
 import java.util.function.BiFunction;
 import java.util.function.Consumer;
 import java.util.function.Function;
+import java.util.stream.Collectors;
 
 /**
  * Like {@link ObjectParser} but works with objects that have constructors whose arguments are mixed in with its other settings. Queries are
@@ -82,7 +87,8 @@ public final class ConstructingObjectParser<Value, Context> extends AbstractObje
     /**
      * List of constructor names used for generating the error message if not all arrive.
      */
-    private final List<ConstructorArgInfo> constructorArgInfos = new ArrayList<>();
+    private final Map<RestApiCompatibleVersion, List<ConstructorArgInfo>> constructorArgInfos =
+        new EnumMap<>(RestApiCompatibleVersion.class);
     private final ObjectParser<Target, Context> objectParser;
     private final BiFunction<Object[], Context, Value> builder;
     /**
@@ -205,8 +211,8 @@ public final class ConstructingObjectParser<Value, Context> extends AbstractObje
              * constructor in the argument list so we don't need to do any fancy
              * or expensive lookups whenever the constructor args come in.
              */
-            int position = addConstructorArg(consumer, parseField);
-            objectParser.declareField((target, v) -> target.constructorArg(position, v), parser, parseField, type);
+            Map<RestApiCompatibleVersion, Integer> positions = addConstructorArg(consumer, parseField);
+            objectParser.declareField((target, v) -> target.constructorArg(positions, v), parser, parseField, type);
         } else {
             numberOfFields += 1;
             objectParser.declareField(queueingConsumer(consumer, parseField), parser, parseField, type);
@@ -234,8 +240,8 @@ public final class ConstructingObjectParser<Value, Context> extends AbstractObje
              * constructor in the argument list so we don't need to do any fancy
              * or expensive lookups whenever the constructor args come in.
              */
-            int position = addConstructorArg(consumer, parseField);
-            objectParser.declareNamedObject((target, v) -> target.constructorArg(position, v), namedObjectParser, parseField);
+            Map<RestApiCompatibleVersion, Integer> positions = addConstructorArg(consumer, parseField);
+            objectParser.declareNamedObject((target, v) -> target.constructorArg(positions, v), namedObjectParser, parseField);
         } else {
             numberOfFields += 1;
             objectParser.declareNamedObject(queueingConsumer(consumer, parseField), namedObjectParser, parseField);
@@ -264,8 +270,8 @@ public final class ConstructingObjectParser<Value, Context> extends AbstractObje
              * constructor in the argument list so we don't need to do any fancy
              * or expensive lookups whenever the constructor args come in.
              */
-            int position = addConstructorArg(consumer, parseField);
-            objectParser.declareNamedObjects((target, v) -> target.constructorArg(position, v), namedObjectParser, parseField);
+            Map<RestApiCompatibleVersion, Integer> positions = addConstructorArg(consumer, parseField);
+            objectParser.declareNamedObjects((target, v) -> target.constructorArg(positions, v), namedObjectParser, parseField);
         } else {
             numberOfFields += 1;
             objectParser.declareNamedObjects(queueingConsumer(consumer, parseField), namedObjectParser, parseField);
@@ -296,18 +302,21 @@ public final class ConstructingObjectParser<Value, Context> extends AbstractObje
              * constructor in the argument list so we don't need to do any fancy
              * or expensive lookups whenever the constructor args come in.
              */
-            int position = addConstructorArg(consumer, parseField);
-            objectParser.declareNamedObjects((target, v) -> target.constructorArg(position, v), namedObjectParser,
-                    wrapOrderedModeCallBack(orderedModeCallback), parseField);
+            Map<RestApiCompatibleVersion, Integer> positions = addConstructorArg(consumer, parseField);
+            objectParser.declareNamedObjects((target, v) -> target.constructorArg(positions, v), namedObjectParser,
+                wrapOrderedModeCallBack(orderedModeCallback), parseField);
         } else {
             numberOfFields += 1;
             objectParser.declareNamedObjects(queueingConsumer(consumer, parseField), namedObjectParser,
-                    wrapOrderedModeCallBack(orderedModeCallback), parseField);
+                wrapOrderedModeCallBack(orderedModeCallback), parseField);
         }
     }
 
     int getNumberOfFields() {
-        return this.constructorArgInfos.size();
+        assert this.constructorArgInfos.get(RestApiCompatibleVersion.currentVersion()).size()
+            == this.constructorArgInfos.get(RestApiCompatibleVersion.minimumSupported()).size() :
+            "Constructors must have same number of arguments per all compatible versions";
+        return this.constructorArgInfos.get(RestApiCompatibleVersion.currentVersion()).size();
     }
 
     /**
@@ -324,11 +333,17 @@ public final class ConstructingObjectParser<Value, Context> extends AbstractObje
      * @param parseField Parse field
      * @return The argument position
      */
-    private int addConstructorArg(BiConsumer<?, ?> consumer, ParseField parseField) {
-        int position = constructorArgInfos.size();
+    private Map<RestApiCompatibleVersion, Integer> addConstructorArg(BiConsumer<?, ?> consumer, ParseField parseField) {
+
         boolean required = consumer == REQUIRED_CONSTRUCTOR_ARG_MARKER;
-        constructorArgInfos.add(new ConstructorArgInfo(parseField, required));
-        return position;
+        for (RestApiCompatibleVersion restApiCompatibleVersion : parseField.getRestApiCompatibleVersions()) {
+
+            constructorArgInfos.computeIfAbsent(restApiCompatibleVersion, (v)-> new ArrayList<>())
+                    .add(new ConstructorArgInfo(parseField, required));
+        }
+
+        //calculate the positions for the arguments
+        return constructorArgInfos.entrySet().stream().collect(Collectors.toMap(Map.Entry::getKey, e -> e.getValue().size()));
     }
 
     @Override
@@ -398,7 +413,7 @@ public final class ConstructingObjectParser<Value, Context> extends AbstractObje
         /**
          * Array of constructor args to be passed to the {@link ConstructingObjectParser#builder}.
          */
-        private final Object[] constructorArgs = new Object[constructorArgInfos.size()];
+        private final Object[] constructorArgs;
         /**
          * The parser this class is working against. We store it here so we can fetch it conveniently when queueing fields to lookup the
          * location of each field so that we can give a useful error message when replaying the queue.
@@ -437,15 +452,18 @@ public final class ConstructingObjectParser<Value, Context> extends AbstractObje
         Target(XContentParser parser, Context context) {
             this.parser = parser;
             this.context = context;
+            this.constructorArgs = new Object[constructorArgInfos
+                .getOrDefault(parser.getRestApiCompatibleVersion(), Collections.emptyList()).size()];
         }
 
         /**
          * Set a constructor argument and build the target object if all constructor arguments have arrived.
          */
-        private void constructorArg(int position, Object value) {
+        private void constructorArg(Map<RestApiCompatibleVersion, Integer> positions, Object value) {
+            int position = positions.get(parser.getRestApiCompatibleVersion()) - 1;
             constructorArgs[position] = value;
             constructorArgsCollected++;
-            if (constructorArgsCollected == constructorArgInfos.size()) {
+            if (constructorArgsCollected == constructorArgInfos.get(parser.getRestApiCompatibleVersion()).size()) {
                 buildTarget();
             }
         }
@@ -480,7 +498,7 @@ public final class ConstructingObjectParser<Value, Context> extends AbstractObje
             StringBuilder message = null;
             for (int i = 0; i < constructorArgs.length; i++) {
                 if (constructorArgs[i] != null) continue;
-                ConstructorArgInfo arg = constructorArgInfos.get(i);
+                ConstructorArgInfo arg = constructorArgInfos.get(parser.getRestApiCompatibleVersion()).get(i);
                 if (false == arg.required) continue;
                 if (message == null) {
                     message = new StringBuilder("Required [").append(arg.field);

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

@@ -9,11 +9,13 @@ package org.elasticsearch.common.xcontent;
 
 import org.elasticsearch.common.Nullable;
 import org.elasticsearch.common.ParseField;
+import org.elasticsearch.common.compatibility.RestApiCompatibleVersion;
 
 import java.io.IOException;
 import java.lang.reflect.Array;
 import java.util.ArrayList;
 import java.util.Arrays;
+import java.util.Collections;
 import java.util.EnumSet;
 import java.util.HashMap;
 import java.util.HashSet;
@@ -88,7 +90,9 @@ public final class ObjectParser<Value, Context> extends AbstractObjectParser<Val
 
     private static <Value, Context> UnknownFieldParser<Value, Context> errorOnUnknown() {
         return (op, f, l, p, v, c) -> {
-            throw new XContentParseException(l, ErrorOnUnknown.IMPLEMENTATION.errorMessage(op.name, f, op.fieldParserMap.keySet()));
+            throw new XContentParseException(l, ErrorOnUnknown.IMPLEMENTATION.errorMessage(op.name, f,
+                op.fieldParserMap.getOrDefault(p.getRestApiCompatibleVersion(), Collections.emptyMap())
+                .keySet()));
         };
     }
 
@@ -137,7 +141,9 @@ public final class ObjectParser<Value, Context> extends AbstractObjectParser<Val
             try {
                 o = parser.namedObject(categoryClass, field, context);
             } catch (NamedObjectNotFoundException e) {
-                Set<String> candidates = new HashSet<>(objectParser.fieldParserMap.keySet());
+                Set<String> candidates = new HashSet<>(objectParser.fieldParserMap
+                    .getOrDefault(parser.getRestApiCompatibleVersion(), Collections.emptyMap())
+                    .keySet());
                 e.getCandidates().forEach(candidates::add);
                 String message = ErrorOnUnknown.IMPLEMENTATION.errorMessage(objectParser.name, field, candidates);
                 throw new XContentParseException(location, message, e);
@@ -146,7 +152,7 @@ public final class ObjectParser<Value, Context> extends AbstractObjectParser<Val
         };
     }
 
-    private final Map<String, FieldParser> fieldParserMap = new HashMap<>();
+    private final Map<RestApiCompatibleVersion, Map<String, FieldParser>> fieldParserMap = new HashMap<>();
     private final String name;
     private final Function<Context, Value> valueBuilder;
     private final UnknownFieldParser<Value, Context> unknownFieldParser;
@@ -277,7 +283,8 @@ public final class ObjectParser<Value, Context> extends AbstractObjectParser<Val
             if (token == XContentParser.Token.FIELD_NAME) {
                 currentFieldName = parser.currentName();
                 currentPosition = parser.getTokenLocation();
-                fieldParser = fieldParserMap.get(currentFieldName);
+                fieldParser = fieldParserMap.getOrDefault(parser.getRestApiCompatibleVersion(), Collections.emptyMap())
+                    .get(currentFieldName);
             } else {
                 if (currentFieldName == null) {
                     throw new XContentParseException(parser.getTokenLocation(), "[" + name  + "] no field found");
@@ -359,8 +366,16 @@ public final class ObjectParser<Value, Context> extends AbstractObjectParser<Val
         }
         FieldParser fieldParser = new FieldParser(p, type.supportedTokens(), parseField, type);
         for (String fieldValue : parseField.getAllNamesIncludedDeprecated()) {
-            fieldParserMap.putIfAbsent(fieldValue, fieldParser);
+            if (parseField.getRestApiCompatibleVersions().contains(RestApiCompatibleVersion.minimumSupported())) {
+                fieldParserMap.putIfAbsent(RestApiCompatibleVersion.minimumSupported(), new HashMap<>());
+                fieldParserMap.get(RestApiCompatibleVersion.minimumSupported()).putIfAbsent(fieldValue, fieldParser);
+            }
+            if (parseField.getRestApiCompatibleVersions().contains(RestApiCompatibleVersion.currentVersion())) {
+                fieldParserMap.putIfAbsent(RestApiCompatibleVersion.currentVersion(), new HashMap<>());
+                fieldParserMap.get(RestApiCompatibleVersion.currentVersion()).putIfAbsent(fieldValue, fieldParser);
+            }
         }
+
     }
 
     @Override
@@ -657,7 +672,7 @@ public final class ObjectParser<Value, Context> extends AbstractObjectParser<Val
     public String toString() {
         return "ObjectParser{" +
                 "name='" + name + '\'' +
-                ", fields=" + fieldParserMap.values() +
+                ", fields=" + fieldParserMap +
                 '}';
     }
 }

+ 133 - 0
libs/x-content/src/test/java/org/elasticsearch/common/xcontent/ConstructingObjectParserTests.java

@@ -11,6 +11,8 @@ package org.elasticsearch.common.xcontent;
 import org.elasticsearch.common.CheckedFunction;
 import org.elasticsearch.common.Nullable;
 import org.elasticsearch.common.ParseField;
+import org.elasticsearch.common.compatibility.RestApiCompatibleVersion;
+import org.elasticsearch.common.logging.DeprecationLogger;
 import org.elasticsearch.common.xcontent.ObjectParserTests.NamedObject;
 import org.elasticsearch.common.xcontent.json.JsonXContent;
 import org.elasticsearch.test.ESTestCase;
@@ -19,6 +21,7 @@ import org.hamcrest.Matcher;
 import java.io.ByteArrayOutputStream;
 import java.io.IOException;
 import java.util.List;
+import java.util.function.BiConsumer;
 
 import static org.elasticsearch.common.xcontent.ConstructingObjectParser.constructorArg;
 import static org.elasticsearch.common.xcontent.ConstructingObjectParser.optionalConstructorArg;
@@ -558,4 +561,134 @@ public class ConstructingObjectParserTests extends ESTestCase {
         e = expectThrows(IllegalArgumentException.class, () -> parser.parse(notenough, null));
         assertThat(e.getMessage(), containsString("Required one of fields [a, b], but none were specified."));
     }
+
+    //migrating name and type from old_string_name:String to new_int_name:int
+    public static class StructWithCompatibleFields {
+        // real usage would have RestApiCompatibleVersion.V_7 instead of currentVersion or minimumSupported
+        static final ConstructingObjectParser<StructWithCompatibleFields, Void> PARSER =
+            new ConstructingObjectParser<>("struct_with_compatible_fields",  a -> new StructWithCompatibleFields((Integer)a[0]));
+
+        static {
+            // declare a field with `new_name` being preferable, and old_name deprecated.
+            // The declaration is only available for lookup when parser has compatibility set
+            PARSER.declareInt(constructorArg(),
+                new ParseField("new_name", "old_name")
+                    .withRestApiCompatibilityVersions(RestApiCompatibleVersion.minimumSupported()));
+
+            // declare `new_name` to be parsed when compatibility is NOT used
+            PARSER.declareInt(constructorArg(),
+                new ParseField("new_name").withRestApiCompatibilityVersions(RestApiCompatibleVersion.currentVersion()));
+
+            // declare `old_name` to throw exception when compatibility is NOT used
+            PARSER.declareInt((r,s) -> failWithException(),
+                new ParseField("old_name").withRestApiCompatibilityVersions(RestApiCompatibleVersion.currentVersion()));
+        }
+        private int intField;
+
+        public StructWithCompatibleFields(int intField) {
+            this.intField = intField;
+        }
+
+        private static void failWithException() {
+            throw new IllegalArgumentException("invalid parameter [old_name], use [new_name] instead");
+        }
+    }
+
+    public void testCompatibleFieldDeclarations() throws IOException {
+        {
+            // new_name is the only way to parse when compatibility is not set
+            XContentParser parser = createParserWithCompatibilityFor(JsonXContent.jsonXContent, "{\"new_name\": 1}",
+                RestApiCompatibleVersion.currentVersion());
+            StructWithCompatibleFields o = StructWithCompatibleFields.PARSER.parse(parser, null);
+            assertEquals(1, o.intField);
+        }
+
+        {
+            // old_name results with an exception when compatibility is not set
+            XContentParser parser = createParserWithCompatibilityFor(JsonXContent.jsonXContent, "{\"old_name\": 1}",
+                RestApiCompatibleVersion.currentVersion());
+            expectThrows(IllegalArgumentException.class, () -> StructWithCompatibleFields.PARSER.parse(parser, null));
+        }
+        {
+            // new_name is allowed to be parsed with compatibility
+            XContentParser parser = createParserWithCompatibilityFor(JsonXContent.jsonXContent, "{\"new_name\": 1}",
+                RestApiCompatibleVersion.minimumSupported());
+            StructWithCompatibleFields o = StructWithCompatibleFields.PARSER.parse(parser, null);
+            assertEquals(1, o.intField);
+        }
+        {
+
+            // old_name is allowed to be parsed with compatibility, but results in deprecation
+            XContentParser parser = createParserWithCompatibilityFor(JsonXContent.jsonXContent, "{\"old_name\": 1}",
+                RestApiCompatibleVersion.minimumSupported());
+            StructWithCompatibleFields o = StructWithCompatibleFields.PARSER.parse(parser, null);
+            assertEquals(1, o.intField);
+            assertWarnings(false, "[struct_with_compatible_fields][1:14] " +
+                "Deprecated field [old_name] used, expected [new_name] instead");
+
+        }
+    }
+
+    // an example on how to support a removed field
+    public static class StructRemovalField {
+        private static final DeprecationLogger deprecationLogger = DeprecationLogger.getLogger(StructRemovalField.class);
+
+        // real usage would have RestApiCompatibleVersion.V_7 instead of currentVersion or minimumSupported
+        static final ConstructingObjectParser<StructRemovalField, Void> PARSER =
+            new ConstructingObjectParser<>("struct_removal", a -> new StructRemovalField((String)a[0]));
+
+        static {
+            //we still need to have something to pass to a constructor. Otherwise use ObjectParser
+            PARSER.declareString(constructorArg(), new ParseField("second_field"));
+
+
+
+            // declare a field with `old_name` being preferable, no deprecated name.
+            // deprecated field name results in a deprecation warning with a suggestion what field to use.
+            // the field was removed so there is nothing to suggest.
+            // The deprecation shoudl be done manually
+            PARSER.declareInt(logWarningDoNothing("old_name"),
+                new ParseField("old_name")
+                    .withRestApiCompatibilityVersions(RestApiCompatibleVersion.minimumSupported()));
+
+            // declare `old_name` to throw exception when compatibility is NOT used
+            PARSER.declareInt((r,s) -> failWithException(),
+                new ParseField("old_name").withRestApiCompatibilityVersions(RestApiCompatibleVersion.currentVersion()));
+        }
+
+        private final String secondField;
+
+        public StructRemovalField(String secondField) {
+            this.secondField = secondField;
+        }
+
+        private static BiConsumer<StructRemovalField, Integer> logWarningDoNothing(String old_name) {
+            return (struct,value) -> deprecationLogger.compatibleApiWarning("struct_removal",
+                "The field old_name has been removed and is being ignored");
+        }
+
+        private static void failWithException() {
+            throw new IllegalArgumentException("invalid parameter [old_name], use [new_name] instead");
+        }
+    }
+
+    public void testRemovalOfField() throws IOException {
+        {
+            // old_name with NO compatibility is resulting in an exception
+            XContentParser parser = createParserWithCompatibilityFor(JsonXContent.jsonXContent,
+                "{\"old_name\": 1, \"second_field\": \"someString\"}",
+                RestApiCompatibleVersion.currentVersion());
+            expectThrows(XContentParseException.class, () -> StructRemovalField.PARSER.parse(parser, null));
+        }
+
+        {
+            // old_name with compatibility is still parsed, but ignored and results in a warning
+            XContentParser parser = createParserWithCompatibilityFor(JsonXContent.jsonXContent,
+                "{\"old_name\": 1, \"second_field\": \"someString\"}",
+                RestApiCompatibleVersion.minimumSupported());
+            StructRemovalField parse = StructRemovalField.PARSER.parse(parser, null);
+
+            assertWarnings("The field old_name has been removed and is being ignored");
+        }
+    }
 }

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

@@ -10,6 +10,7 @@ package org.elasticsearch.common.xcontent;
 import org.elasticsearch.common.CheckedFunction;
 import org.elasticsearch.common.ParseField;
 import org.elasticsearch.common.Strings;
+import org.elasticsearch.common.compatibility.RestApiCompatibleVersion;
 import org.elasticsearch.common.xcontent.ObjectParser.NamedObjectParser;
 import org.elasticsearch.common.xcontent.ObjectParser.ValueType;
 import org.elasticsearch.common.xcontent.json.JsonXContent;
@@ -63,10 +64,6 @@ public class ObjectParserTests extends ESTestCase {
         assertEquals(s.test, "foo");
         assertEquals(s.testNumber, 2);
         assertEquals(s.ints, Arrays.asList(1, 2, 3, 4));
-        assertEquals(objectParser.toString(), "ObjectParser{name='foo', fields=["
-                + "FieldParser{preferred_name=test, supportedTokens=[VALUE_STRING], type=STRING}, "
-                + "FieldParser{preferred_name=test_array, supportedTokens=[START_ARRAY, VALUE_STRING, VALUE_NUMBER], type=INT_ARRAY}, "
-                + "FieldParser{preferred_name=test_number, supportedTokens=[VALUE_STRING, VALUE_NUMBER], type=INT}]}");
     }
 
     public void testNullDeclares() {
@@ -1010,4 +1007,71 @@ public class ObjectParserTests extends ESTestCase {
         AtomicReference<String> parsed = parser.parse(createParser(JsonXContent.jsonXContent, "{}"), context);
         assertThat(parsed.get(), equalTo(context));
     }
+
+    public static class StructWithCompatibleFields {
+        // real usage would have RestApiCompatibleVersion.V_7 instead of currentVersion or minimumSupported
+
+        static final ObjectParser<StructWithCompatibleFields, Void> PARSER =
+            new ObjectParser<>("struct_with_compatible_fields", StructWithCompatibleFields::new);
+        static {
+            // declare a field with `new_name` being preferable, and old_name deprecated.
+            // The declaration is only available for lookup when parser has compatibility set
+            PARSER.declareInt(StructWithCompatibleFields::setIntField,
+                new ParseField("new_name", "old_name")
+                    .withRestApiCompatibilityVersions(RestApiCompatibleVersion.minimumSupported()));
+
+            // declare `new_name` to be parsed when compatibility is NOT used
+            PARSER.declareInt(StructWithCompatibleFields::setIntField,
+                new ParseField("new_name").withRestApiCompatibilityVersions(RestApiCompatibleVersion.currentVersion()));
+
+            // declare `old_name` to throw exception when compatibility is NOT used
+            PARSER.declareInt((r,s) -> failWithException(),
+                new ParseField("old_name").withRestApiCompatibilityVersions(RestApiCompatibleVersion.currentVersion()));
+        }
+
+        private static void failWithException() {
+            throw new IllegalArgumentException("invalid parameter [old_name], use [new_name] instead");
+        }
+
+        private int intField;
+
+        private  void setIntField(int intField) {
+            this.intField = intField;
+        }
+    }
+
+    public void testCompatibleFieldDeclarations() throws IOException {
+        {
+            // new_name is the only way to parse when compatibility is not set
+            XContentParser parser = createParserWithCompatibilityFor(JsonXContent.jsonXContent, "{\"new_name\": 1}",
+                RestApiCompatibleVersion.currentVersion());
+            StructWithCompatibleFields o = StructWithCompatibleFields.PARSER.parse(parser, null);
+            assertEquals(1, o.intField);
+        }
+
+        {
+            // old_name results with an exception when compatibility is not set
+            XContentParser parser = createParserWithCompatibilityFor(JsonXContent.jsonXContent, "{\"old_name\": 1}",
+                RestApiCompatibleVersion.currentVersion());
+            expectThrows(IllegalArgumentException.class, () -> StructWithCompatibleFields.PARSER.parse(parser, null));
+        }
+        {
+            // new_name is allowed to be parsed with compatibility
+            XContentParser parser = createParserWithCompatibilityFor(JsonXContent.jsonXContent, "{\"new_name\": 1}",
+                RestApiCompatibleVersion.minimumSupported());
+            StructWithCompatibleFields o = StructWithCompatibleFields.PARSER.parse(parser, null);
+            assertEquals(1, o.intField);
+        }
+        {
+
+            // old_name is allowed to be parsed with compatibility, but results in deprecation
+            XContentParser parser = createParserWithCompatibilityFor(JsonXContent.jsonXContent, "{\"old_name\": 1}",
+                RestApiCompatibleVersion.minimumSupported());
+            StructWithCompatibleFields o = StructWithCompatibleFields.PARSER.parse(parser, null);
+            assertEquals(1, o.intField);
+            assertWarnings(false, "[struct_with_compatible_fields][1:14] " +
+                "Deprecated field [old_name] used, expected [new_name] instead");
+
+        }
+    }
 }

+ 14 - 1
test/framework/src/main/java/org/elasticsearch/test/ESTestCase.java

@@ -47,6 +47,7 @@ import org.elasticsearch.common.CheckedRunnable;
 import org.elasticsearch.common.SuppressForbidden;
 import org.elasticsearch.common.bytes.BytesArray;
 import org.elasticsearch.common.bytes.BytesReference;
+import org.elasticsearch.common.compatibility.RestApiCompatibleVersion;
 import org.elasticsearch.common.io.PathUtils;
 import org.elasticsearch.common.io.PathUtilsForTesting;
 import org.elasticsearch.common.io.stream.BytesStreamOutput;
@@ -111,11 +112,13 @@ import org.junit.Rule;
 import org.junit.internal.AssumptionViolatedException;
 import org.junit.rules.RuleChain;
 
+import java.io.ByteArrayInputStream;
 import java.io.IOException;
 import java.io.InputStream;
 import java.math.BigInteger;
 import java.net.InetAddress;
 import java.net.UnknownHostException;
+import java.nio.charset.StandardCharsets;
 import java.nio.file.Path;
 import java.time.ZoneId;
 import java.util.ArrayList;
@@ -1292,7 +1295,11 @@ public abstract class ESTestCase extends LuceneTestCase {
      * Create a new {@link XContentParser}.
      */
     protected final XContentParser createParser(XContent xContent, String data) throws IOException {
-        return xContent.createParser(xContentRegistry(), LoggingDeprecationHandler.INSTANCE, data);
+        if (randomBoolean()) {
+            return createParserWithCompatibilityFor(xContent, data, RestApiCompatibleVersion.minimumSupported());
+        } else {
+            return xContent.createParser(xContentRegistry(), LoggingDeprecationHandler.INSTANCE, data);
+        }
     }
 
     /**
@@ -1329,6 +1336,12 @@ public abstract class ESTestCase extends LuceneTestCase {
         return xContent.createParser(namedXContentRegistry, LoggingDeprecationHandler.INSTANCE, data.streamInput());
     }
 
+    protected final XContentParser createParserWithCompatibilityFor(XContent xContent, String data,
+                                                            RestApiCompatibleVersion restApiCompatibleVersion) throws IOException {
+        return xContent.createParserForCompatibility(xContentRegistry(), LoggingDeprecationHandler.INSTANCE,
+            new ByteArrayInputStream(data.getBytes(StandardCharsets.UTF_8)), restApiCompatibleVersion);
+    }
+
     private static final NamedXContentRegistry DEFAULT_NAMED_X_CONTENT_REGISTRY =
             new NamedXContentRegistry(ClusterModule.getNamedXWriteables());