From 3f64d530b18a200b11f0fa8b6ada0130cbc0f117 Mon Sep 17 00:00:00 2001 From: Alexis Cote Date: Mon, 3 Aug 2026 11:39:42 -0400 Subject: [PATCH 1/2] fix(json): handle pre-consumed parser event in JsonData._DESERIALIZER The lambda-based deserializer ignored the three-argument overload, causing it to re-read from the parser after the event was already consumed. Replace with an explicit JsonpDeserializerBase that delegates to parser.getValue() when the event is provided. --- .../java/org/opensearch/client/json/JsonData.java | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/java-client/src/main/java/org/opensearch/client/json/JsonData.java b/java-client/src/main/java/org/opensearch/client/json/JsonData.java index ad907650bb..0aa99b5363 100644 --- a/java-client/src/main/java/org/opensearch/client/json/JsonData.java +++ b/java-client/src/main/java/org/opensearch/client/json/JsonData.java @@ -108,5 +108,16 @@ static JsonData from(JsonParser parser, JsonpMapper mapper) { return of(parser.getValue(), mapper); } - JsonpDeserializer _DESERIALIZER = JsonpDeserializer.of(EnumSet.allOf(JsonParser.Event.class), JsonData::from); + JsonpDeserializer _DESERIALIZER = new JsonpDeserializerBase(EnumSet.allOf(JsonParser.Event.class)) { + @Override + public JsonData deserialize(JsonParser parser, JsonpMapper mapper) { + return JsonData.from(parser, mapper); + } + + @Override + public JsonData deserialize(JsonParser parser, JsonpMapper mapper, JsonParser.Event event) { + // Event already consumed — getValue() returns the current token's value tree + return JsonData.of(parser.getValue(), mapper); + } + }; } From db4fcfe627d3068519c131cba99046a7d87538c6 Mon Sep 17 00:00:00 2001 From: Alexis Cote Date: Mon, 3 Aug 2026 11:40:20 -0400 Subject: [PATCH 2/2] feat(codegen): add _Custom variant kind to non-exhaustive tagged unions Tagged unions annotated with x-non-exhaustive in the OpenAPI spec now emit a Kind._Custom enum member so that unknown discriminator values (e.g. plugin-provided aggregation types) deserialize into a lossless JsonData wrapper instead of throwing. Affected unions: Aggregate, Query, SpanQuery, Property, Processor, Analyzer, CharFilterDefinition, TokenFilterDefinition, TokenizerDefinition. --- CHANGELOG.md | 2 + guides/json.md | 39 ++++- .../_types/aggregations/Aggregate.java | 96 ++++++++++++- .../opensearch/_types/analysis/Analyzer.java | 92 +++++++++++- .../_types/analysis/CharFilterDefinition.java | 92 +++++++++++- .../analysis/TokenFilterDefinition.java | 92 +++++++++++- .../_types/analysis/TokenizerDefinition.java | 92 +++++++++++- .../opensearch/_types/mapping/Property.java | 92 +++++++++++- .../opensearch/_types/query_dsl/Query.java | 93 +++++++++++- .../_types/query_dsl/SpanQuery.java | 94 +++++++++++- .../client/opensearch/ingest/Processor.java | 94 +++++++++++- .../client/json/ExternallyTaggedUnion.java | 13 +- .../opensearch/client/util/TaggedUnion.java | 8 ++ .../opensearch/model/CustomVariantTest.java | 134 ++++++++++++++++++ java-codegen/opensearch-openapi.yaml | 11 ++ .../codegen/model/TaggedUnionShape.java | 12 ++ .../client/codegen/openapi/OpenApiSchema.java | 17 +++ .../codegen/transformer/SpecTransformer.java | 2 + .../templates/TaggedUnionShape.mustache | 101 ++++++++++++- .../TaggedUnionShape/Deserialize.mustache | 19 +++ .../TaggedUnionShape/Serialize.mustache | 5 + 21 files changed, 1174 insertions(+), 26 deletions(-) create mode 100644 java-client/src/test/java/org/opensearch/client/opensearch/model/CustomVariantTest.java diff --git a/CHANGELOG.md b/CHANGELOG.md index b9ca65d964..4b9e1d383c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,7 @@ Inspired from [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) - Bump `org.apache.httpcomponents.client5:httpclient5` from 5.6 to 5.6.1 ([#1967](https://github.com/opensearch-project/opensearch-java/pull/1967)) ### Added +- Add `_Custom` variant kind to non-exhaustive tagged unions (e.g. `Aggregate`, `Suggest`, `Query`, `Property`, `Processor`) to gracefully handle plugin-provided types without deserialization failures - Run Java client integration tests with a Testcontainers-managed OpenSearch instance by default ([#2033](https://github.com/opensearch-project/opensearch-java/pull/2033)) - Detect AWS SDK `Apache5HttpClient` in `AwsSdk2Transport` body-method guardrail ([#1903](https://github.com/opensearch-project/opensearch-java/pull/1970)) - Support Jackson 3.x release line ([#1810](https://github.com/opensearch-project/opensearch-java/pull/1810)) @@ -25,6 +26,7 @@ Inspired from [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) - Add transparent gRPC transport with HybridTransport (bulk over gRPC, REST fallback), translation layer, TLS, basic auth, AWS SigV4, and JWT support ([#2062](https://github.com/opensearch-project/opensearch-java/pull/2062)) ### Fixed +- Fix `JsonData._DESERIALIZER` to correctly handle a pre-consumed parser event in the three-argument `deserialize` overload ## [Unreleased 3.x] ### Added diff --git a/guides/json.md b/guides/json.md index 86e7f15fb9..d5177a9488 100644 --- a/guides/json.md +++ b/guides/json.md @@ -5,6 +5,9 @@ - [Deserialization](#deserialization) - [Using withJson](#using-withjson) - [Using static _DESERIALIZER](#using-static-_deserializer) + - [Custom (Plugin-Provided) Variant Types](#custom-plugin-provided-variant-types) + - [Reading a custom variant](#reading-a-custom-variant) + - [Building a custom variant](#building-a-custom-variant) # Working With JSON @@ -109,4 +112,38 @@ private IndexTemplateMapping getInstance(String templateJsonString) { return indexTemplateMapping; } } -``` \ No newline at end of file +``` + +## Custom (Plugin-Provided) Variant Types + +Tagged unions marked as non-exhaustive in the OpenAPI specification (`x-non-exhaustive: true`) support a `_Custom` variant kind. This currently includes externally-tagged unions (e.g. `Aggregate`, `Suggest`) and internally-tagged unions (e.g. `Query`, `Property`, `Processor`, `Analyzer`, `TokenFilterDefinition`). + +When the server returns a discriminator value that isn't natively modeled (for example, a type introduced by a plugin), these unions capture it as `Kind._Custom` with the raw JSON preserved as `JsonData`, rather than throwing a deserialization error. + +### Reading a custom variant + +```java +SearchResponse response = client.search(searchRequest, IndexData.class); + +for (Map.Entry entry : response.aggregations().entrySet()) { + Aggregate agg = entry.getValue(); + if (agg._isCustom()) { + // _customKind() returns the type name from the server (e.g. "my_plugin_agg") + String typeName = agg._customKind(); + // _custom() returns the raw JSON body as JsonData + JsonData rawData = agg._custom(); + System.out.printf("Custom aggregation '%s' of type '%s': %s%n", + entry.getKey(), typeName, rawData.toJson()); + } +} +``` + +### Building a custom variant + +```java +Aggregate custom = new Aggregate.Builder() + ._custom("my_plugin_agg", JsonData.of(Map.of("score", 42))) + .build(); +``` + +The same `_isCustom()`, `_customKind()`, `_custom()`, and `Builder._custom(type, data)` methods are available on every tagged union type marked `x-non-exhaustive` in the OpenAPI specification. diff --git a/java-client/src/generated/java/org/opensearch/client/opensearch/_types/aggregations/Aggregate.java b/java-client/src/generated/java/org/opensearch/client/opensearch/_types/aggregations/Aggregate.java index e3b7869072..ab7e676b43 100644 --- a/java-client/src/generated/java/org/opensearch/client/opensearch/_types/aggregations/Aggregate.java +++ b/java-client/src/generated/java/org/opensearch/client/opensearch/_types/aggregations/Aggregate.java @@ -44,6 +44,7 @@ import javax.annotation.Generated; import javax.annotation.Nonnull; import org.opensearch.client.json.ExternallyTaggedUnion; +import org.opensearch.client.json.JsonData; import org.opensearch.client.json.JsonEnum; import org.opensearch.client.json.JsonpDeserializer; import org.opensearch.client.json.JsonpMapper; @@ -122,7 +123,9 @@ public enum Kind implements JsonEnum { Umterms("umterms"), ValueCount("value_count"), VariableWidthHistogram("variable_width_histogram"), - WeightedAvg("weighted_avg"); + WeightedAvg("weighted_avg"), + /** A custom variant type not natively supported by this client. */ + _Custom(null); private final String jsonValue; @@ -138,6 +141,7 @@ public String jsonValue() { private final Kind _kind; private final AggregateVariant _value; + private final String _customKind; @Override public final Kind _kind() { @@ -149,14 +153,23 @@ public final AggregateVariant _get() { return _value; } + /** + * Returns the actual type name when {@code _kind() == Kind._Custom}, otherwise {@code null}. + */ + public final String _customKind() { + return _customKind; + } + public Aggregate(AggregateVariant value) { this._kind = ApiTypeHelper.requireNonNull(value._aggregateKind(), this, ""); this._value = ApiTypeHelper.requireNonNull(value, this, ""); + this._customKind = null; } private Aggregate(Builder builder) { this._kind = ApiTypeHelper.requireNonNull(builder._kind, builder, ""); this._value = ApiTypeHelper.requireNonNull(builder._value, builder, ""); + this._customKind = builder._customKind; } public static Aggregate of(Function> fn) { @@ -1139,6 +1152,25 @@ public WeightedAvgAggregate weightedAvg() { return TaggedUnionUtils.get(this, Kind.WeightedAvg); } + /** + * Is this variant instance of kind {@code _custom}? + */ + public boolean _isCustom() { + return _kind == Kind._Custom; + } + + /** + * Get the raw JSON data for a custom (plugin-provided) variant type. + * + * @throws IllegalStateException if the current variant is not the {@code _custom} kind. + */ + public JsonData _custom() { + if (_kind != Kind._Custom) { + throw new IllegalStateException("Expected variant kind '_custom' but got '" + _kind + "'"); + } + return ((CustomVariant) _value).data(); + } + @Override public void serialize(JsonGenerator generator, JsonpMapper mapper) { mapper.serialize(_value, generator); @@ -1157,12 +1189,14 @@ public static Builder builder() { public static class Builder extends ObjectBuilderBase implements ObjectBuilder { private Kind _kind; private AggregateVariant _value; + private String _customKind; public Builder() {} private Builder(Aggregate o) { this._kind = o._kind; this._value = o._value; + this._customKind = o._customKind; } public ObjectBuilder adjacencyMatrix(AdjacencyMatrixAggregate v) { @@ -1809,6 +1843,19 @@ public ObjectBuilder weightedAvg(Function _custom(String type, JsonData data) { + this._kind = Kind._Custom; + this._customKind = ApiTypeHelper.requireNonNull(type, this, ""); + this._value = new CustomVariant(ApiTypeHelper.requireNonNull(data, this, "")); + return this; + } + @Override public Aggregate build() { _checkSingleUse(); @@ -1818,6 +1865,10 @@ public Aggregate build() { public static final ExternallyTaggedUnion.TypedKeysDeserializer _TYPED_KEYS_DESERIALIZER; + private static Aggregate _buildCustom(String type, JsonData data) { + return new Builder()._custom(type, data).build(); + } + static { Map> deserializers = new HashMap<>(); deserializers.put("adjacency_matrix", AdjacencyMatrixAggregate._DESERIALIZER); @@ -1882,7 +1933,8 @@ public Aggregate build() { deserializers.put("variable_width_histogram", VariableWidthHistogramAggregate._DESERIALIZER); deserializers.put("weighted_avg", WeightedAvgAggregate._DESERIALIZER); - _TYPED_KEYS_DESERIALIZER = new ExternallyTaggedUnion.Deserializer<>(deserializers, Aggregate::new).typedKeys(); + _TYPED_KEYS_DESERIALIZER = new ExternallyTaggedUnion.Deserializer<>(deserializers, Aggregate::new, Aggregate::_buildCustom) + .typedKeys(); } @Override @@ -1890,6 +1942,7 @@ public int hashCode() { int result = 17; result = 31 * result + Objects.hashCode(this._kind); result = 31 * result + Objects.hashCode(this._value); + result = 31 * result + Objects.hashCode(this._customKind); return result; } @@ -1898,6 +1951,43 @@ public boolean equals(Object o) { if (this == o) return true; if (o == null || this.getClass() != o.getClass()) return false; Aggregate other = (Aggregate) o; - return Objects.equals(this._kind, other._kind) && Objects.equals(this._value, other._value); + return Objects.equals(this._kind, other._kind) + && Objects.equals(this._value, other._value) + && Objects.equals(this._customKind, other._customKind); + } + + // Wrapper so JsonData fits the variant interface slot for custom/plugin types + private static final class CustomVariant implements AggregateVariant, PlainJsonSerializable { + private final JsonData data; + + CustomVariant(JsonData data) { + this.data = data; + } + + public JsonData data() { + return data; + } + + @Override + public Kind _aggregateKind() { + return Kind._Custom; + } + + @Override + public void serialize(JsonGenerator generator, JsonpMapper mapper) { + data.serialize(generator, mapper); + } + + @Override + public int hashCode() { + return data.hashCode(); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + return data.equals(((CustomVariant) o).data); + } } } diff --git a/java-client/src/generated/java/org/opensearch/client/opensearch/_types/analysis/Analyzer.java b/java-client/src/generated/java/org/opensearch/client/opensearch/_types/analysis/Analyzer.java index 611502d7f1..13de047755 100644 --- a/java-client/src/generated/java/org/opensearch/client/opensearch/_types/analysis/Analyzer.java +++ b/java-client/src/generated/java/org/opensearch/client/opensearch/_types/analysis/Analyzer.java @@ -41,6 +41,7 @@ import java.util.function.Function; import javax.annotation.Generated; import javax.annotation.Nonnull; +import org.opensearch.client.json.JsonData; import org.opensearch.client.json.JsonEnum; import org.opensearch.client.json.JsonpDeserializable; import org.opensearch.client.json.JsonpDeserializer; @@ -80,7 +81,9 @@ public enum Kind implements JsonEnum { Snowball("snowball"), Standard("standard"), Stop("stop"), - Whitespace("whitespace"); + Whitespace("whitespace"), + /** A custom variant type not natively supported by this client. */ + _Custom(null); private final String jsonValue; @@ -96,6 +99,7 @@ public String jsonValue() { private final Kind _kind; private final AnalyzerVariant _value; + private final String _customKind; @Override public final Kind _kind() { @@ -107,14 +111,23 @@ public final AnalyzerVariant _get() { return _value; } + /** + * Returns the actual type name when {@code _kind() == Kind._Custom}, otherwise {@code null}. + */ + public final String _customKind() { + return _customKind; + } + public Analyzer(AnalyzerVariant value) { this._kind = ApiTypeHelper.requireNonNull(value._analyzerKind(), this, ""); this._value = ApiTypeHelper.requireNonNull(value, this, ""); + this._customKind = null; } private Analyzer(Builder builder) { this._kind = ApiTypeHelper.requireNonNull(builder._kind, builder, ""); this._value = ApiTypeHelper.requireNonNull(builder._value, builder, ""); + this._customKind = builder._customKind; } public static Analyzer of(Function> fn) { @@ -409,6 +422,25 @@ public WhitespaceAnalyzer whitespace() { return TaggedUnionUtils.get(this, Kind.Whitespace); } + /** + * Is this variant instance of kind {@code _custom}? + */ + public boolean _isCustom() { + return _kind == Kind._Custom; + } + + /** + * Get the raw JSON data for a custom (plugin-provided) variant type. + * + * @throws IllegalStateException if the current variant is not the {@code _custom} kind. + */ + public JsonData _custom() { + if (_kind != Kind._Custom) { + throw new IllegalStateException("Expected variant kind '_custom' but got '" + _kind + "'"); + } + return ((CustomVariant) _value).data(); + } + @Override public void serialize(JsonGenerator generator, JsonpMapper mapper) { mapper.serialize(_value, generator); @@ -427,12 +459,14 @@ public static Builder builder() { public static class Builder extends ObjectBuilderBase implements ObjectBuilder { private Kind _kind; private AnalyzerVariant _value; + private String _customKind; public Builder() {} private Builder(Analyzer o) { this._kind = o._kind; this._value = o._value; + this._customKind = o._customKind; } public ObjectBuilder cjk(CjkAnalyzer v) { @@ -615,6 +649,19 @@ public ObjectBuilder whitespace(Function _custom(String type, JsonData data) { + this._kind = Kind._Custom; + this._customKind = ApiTypeHelper.requireNonNull(type, this, ""); + this._value = new CustomVariant(ApiTypeHelper.requireNonNull(data, this, "")); + return this; + } + @Override public Analyzer build() { _checkSingleUse(); @@ -642,6 +689,9 @@ protected static void setupAnalyzerDeserializer(ObjectDeserializer op) op.add(Builder::stop, StopAnalyzer._DESERIALIZER, "stop"); op.add(Builder::whitespace, WhitespaceAnalyzer._DESERIALIZER, "whitespace"); op.setTypeProperty("type", Kind.Custom.jsonValue()); + op.setUnknownFieldHandler( + (builder, name, parser, mapper) -> builder._custom(name, JsonData._DESERIALIZER.deserialize(parser, mapper)) + ); } public static final JsonpDeserializer _DESERIALIZER = ObjectBuilderDeserializer.lazy( @@ -655,6 +705,7 @@ public int hashCode() { int result = 17; result = 31 * result + Objects.hashCode(this._kind); result = 31 * result + Objects.hashCode(this._value); + result = 31 * result + Objects.hashCode(this._customKind); return result; } @@ -663,6 +714,43 @@ public boolean equals(Object o) { if (this == o) return true; if (o == null || this.getClass() != o.getClass()) return false; Analyzer other = (Analyzer) o; - return Objects.equals(this._kind, other._kind) && Objects.equals(this._value, other._value); + return Objects.equals(this._kind, other._kind) + && Objects.equals(this._value, other._value) + && Objects.equals(this._customKind, other._customKind); + } + + // Wrapper so JsonData fits the variant interface slot for custom/plugin types + private static final class CustomVariant implements AnalyzerVariant, PlainJsonSerializable { + private final JsonData data; + + CustomVariant(JsonData data) { + this.data = data; + } + + public JsonData data() { + return data; + } + + @Override + public Kind _analyzerKind() { + return Kind._Custom; + } + + @Override + public void serialize(JsonGenerator generator, JsonpMapper mapper) { + data.serialize(generator, mapper); + } + + @Override + public int hashCode() { + return data.hashCode(); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + return data.equals(((CustomVariant) o).data); + } } } diff --git a/java-client/src/generated/java/org/opensearch/client/opensearch/_types/analysis/CharFilterDefinition.java b/java-client/src/generated/java/org/opensearch/client/opensearch/_types/analysis/CharFilterDefinition.java index 4763559d01..e472158b3f 100644 --- a/java-client/src/generated/java/org/opensearch/client/opensearch/_types/analysis/CharFilterDefinition.java +++ b/java-client/src/generated/java/org/opensearch/client/opensearch/_types/analysis/CharFilterDefinition.java @@ -41,6 +41,7 @@ import java.util.function.Function; import javax.annotation.Generated; import javax.annotation.Nonnull; +import org.opensearch.client.json.JsonData; import org.opensearch.client.json.JsonEnum; import org.opensearch.client.json.JsonpDeserializable; import org.opensearch.client.json.JsonpDeserializer; @@ -67,7 +68,9 @@ public enum Kind implements JsonEnum { IcuNormalizer("icu_normalizer"), KuromojiIterationMark("kuromoji_iteration_mark"), Mapping("mapping"), - PatternReplace("pattern_replace"); + PatternReplace("pattern_replace"), + /** A custom variant type not natively supported by this client. */ + _Custom(null); private final String jsonValue; @@ -83,6 +86,7 @@ public String jsonValue() { private final Kind _kind; private final CharFilterDefinitionVariant _value; + private final String _customKind; @Override public final Kind _kind() { @@ -94,14 +98,23 @@ public final CharFilterDefinitionVariant _get() { return _value; } + /** + * Returns the actual type name when {@code _kind() == Kind._Custom}, otherwise {@code null}. + */ + public final String _customKind() { + return _customKind; + } + public CharFilterDefinition(CharFilterDefinitionVariant value) { this._kind = ApiTypeHelper.requireNonNull(value._charFilterDefinitionKind(), this, ""); this._value = ApiTypeHelper.requireNonNull(value, this, ""); + this._customKind = null; } private CharFilterDefinition(Builder builder) { this._kind = ApiTypeHelper.requireNonNull(builder._kind, builder, ""); this._value = ApiTypeHelper.requireNonNull(builder._value, builder, ""); + this._customKind = builder._customKind; } public static CharFilterDefinition of(Function> fn) { @@ -188,6 +201,25 @@ public PatternReplaceCharFilter patternReplace() { return TaggedUnionUtils.get(this, Kind.PatternReplace); } + /** + * Is this variant instance of kind {@code _custom}? + */ + public boolean _isCustom() { + return _kind == Kind._Custom; + } + + /** + * Get the raw JSON data for a custom (plugin-provided) variant type. + * + * @throws IllegalStateException if the current variant is not the {@code _custom} kind. + */ + public JsonData _custom() { + if (_kind != Kind._Custom) { + throw new IllegalStateException("Expected variant kind '_custom' but got '" + _kind + "'"); + } + return ((CustomVariant) _value).data(); + } + @Override public void serialize(JsonGenerator generator, JsonpMapper mapper) { mapper.serialize(_value, generator); @@ -206,12 +238,14 @@ public static Builder builder() { public static class Builder extends ObjectBuilderBase implements ObjectBuilder { private Kind _kind; private CharFilterDefinitionVariant _value; + private String _customKind; public Builder() {} private Builder(CharFilterDefinition o) { this._kind = o._kind; this._value = o._value; + this._customKind = o._customKind; } public ObjectBuilder htmlStrip(HtmlStripCharFilter v) { @@ -270,6 +304,19 @@ public ObjectBuilder patternReplace( return this.patternReplace(fn.apply(new PatternReplaceCharFilter.Builder()).build()); } + /** + * Set a custom (plugin-provided) variant. + * + * @param type the variant type name as returned by the server + * @param data the raw JSON body of the variant result + */ + public ObjectBuilder _custom(String type, JsonData data) { + this._kind = Kind._Custom; + this._customKind = ApiTypeHelper.requireNonNull(type, this, ""); + this._value = new CustomVariant(ApiTypeHelper.requireNonNull(data, this, "")); + return this; + } + @Override public CharFilterDefinition build() { _checkSingleUse(); @@ -284,6 +331,9 @@ protected static void setupCharFilterDefinitionDeserializer(ObjectDeserializer builder._custom(name, JsonData._DESERIALIZER.deserialize(parser, mapper)) + ); } public static final JsonpDeserializer _DESERIALIZER = ObjectBuilderDeserializer.lazy( @@ -297,6 +347,7 @@ public int hashCode() { int result = 17; result = 31 * result + Objects.hashCode(this._kind); result = 31 * result + Objects.hashCode(this._value); + result = 31 * result + Objects.hashCode(this._customKind); return result; } @@ -305,6 +356,43 @@ public boolean equals(Object o) { if (this == o) return true; if (o == null || this.getClass() != o.getClass()) return false; CharFilterDefinition other = (CharFilterDefinition) o; - return Objects.equals(this._kind, other._kind) && Objects.equals(this._value, other._value); + return Objects.equals(this._kind, other._kind) + && Objects.equals(this._value, other._value) + && Objects.equals(this._customKind, other._customKind); + } + + // Wrapper so JsonData fits the variant interface slot for custom/plugin types + private static final class CustomVariant implements CharFilterDefinitionVariant, PlainJsonSerializable { + private final JsonData data; + + CustomVariant(JsonData data) { + this.data = data; + } + + public JsonData data() { + return data; + } + + @Override + public Kind _charFilterDefinitionKind() { + return Kind._Custom; + } + + @Override + public void serialize(JsonGenerator generator, JsonpMapper mapper) { + data.serialize(generator, mapper); + } + + @Override + public int hashCode() { + return data.hashCode(); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + return data.equals(((CustomVariant) o).data); + } } } diff --git a/java-client/src/generated/java/org/opensearch/client/opensearch/_types/analysis/TokenFilterDefinition.java b/java-client/src/generated/java/org/opensearch/client/opensearch/_types/analysis/TokenFilterDefinition.java index 56bd2a8e00..e288553ea6 100644 --- a/java-client/src/generated/java/org/opensearch/client/opensearch/_types/analysis/TokenFilterDefinition.java +++ b/java-client/src/generated/java/org/opensearch/client/opensearch/_types/analysis/TokenFilterDefinition.java @@ -41,6 +41,7 @@ import java.util.function.Function; import javax.annotation.Generated; import javax.annotation.Nonnull; +import org.opensearch.client.json.JsonData; import org.opensearch.client.json.JsonEnum; import org.opensearch.client.json.JsonpDeserializable; import org.opensearch.client.json.JsonpDeserializer; @@ -112,7 +113,9 @@ public enum Kind implements JsonEnum { Unique("unique"), Uppercase("uppercase"), WordDelimiter("word_delimiter"), - WordDelimiterGraph("word_delimiter_graph"); + WordDelimiterGraph("word_delimiter_graph"), + /** A custom variant type not natively supported by this client. */ + _Custom(null); private final String jsonValue; @@ -128,6 +131,7 @@ public String jsonValue() { private final Kind _kind; private final TokenFilterDefinitionVariant _value; + private final String _customKind; @Override public final Kind _kind() { @@ -139,14 +143,23 @@ public final TokenFilterDefinitionVariant _get() { return _value; } + /** + * Returns the actual type name when {@code _kind() == Kind._Custom}, otherwise {@code null}. + */ + public final String _customKind() { + return _customKind; + } + public TokenFilterDefinition(TokenFilterDefinitionVariant value) { this._kind = ApiTypeHelper.requireNonNull(value._tokenFilterDefinitionKind(), this, ""); this._value = ApiTypeHelper.requireNonNull(value, this, ""); + this._customKind = null; } private TokenFilterDefinition(Builder builder) { this._kind = ApiTypeHelper.requireNonNull(builder._kind, builder, ""); this._value = ApiTypeHelper.requireNonNull(builder._value, builder, ""); + this._customKind = builder._customKind; } public static TokenFilterDefinition of(Function> fn) { @@ -953,6 +966,25 @@ public WordDelimiterGraphTokenFilter wordDelimiterGraph() { return TaggedUnionUtils.get(this, Kind.WordDelimiterGraph); } + /** + * Is this variant instance of kind {@code _custom}? + */ + public boolean _isCustom() { + return _kind == Kind._Custom; + } + + /** + * Get the raw JSON data for a custom (plugin-provided) variant type. + * + * @throws IllegalStateException if the current variant is not the {@code _custom} kind. + */ + public JsonData _custom() { + if (_kind != Kind._Custom) { + throw new IllegalStateException("Expected variant kind '_custom' but got '" + _kind + "'"); + } + return ((CustomVariant) _value).data(); + } + @Override public void serialize(JsonGenerator generator, JsonpMapper mapper) { mapper.serialize(_value, generator); @@ -971,12 +1003,14 @@ public static Builder builder() { public static class Builder extends ObjectBuilderBase implements ObjectBuilder { private Kind _kind; private TokenFilterDefinitionVariant _value; + private String _customKind; public Builder() {} private Builder(TokenFilterDefinition o) { this._kind = o._kind; this._value = o._value; + this._customKind = o._customKind; } public ObjectBuilder asciifolding(AsciiFoldingTokenFilter v) { @@ -1545,6 +1579,19 @@ public ObjectBuilder wordDelimiterGraph( return this.wordDelimiterGraph(fn.apply(new WordDelimiterGraphTokenFilter.Builder()).build()); } + /** + * Set a custom (plugin-provided) variant. + * + * @param type the variant type name as returned by the server + * @param data the raw JSON body of the variant result + */ + public ObjectBuilder _custom(String type, JsonData data) { + this._kind = Kind._Custom; + this._customKind = ApiTypeHelper.requireNonNull(type, this, ""); + this._value = new CustomVariant(ApiTypeHelper.requireNonNull(data, this, "")); + return this; + } + @Override public TokenFilterDefinition build() { _checkSingleUse(); @@ -1604,6 +1651,9 @@ protected static void setupTokenFilterDefinitionDeserializer(ObjectDeserializer< op.add(Builder::wordDelimiter, WordDelimiterTokenFilter._DESERIALIZER, "word_delimiter"); op.add(Builder::wordDelimiterGraph, WordDelimiterGraphTokenFilter._DESERIALIZER, "word_delimiter_graph"); op.setTypeProperty("type", null); + op.setUnknownFieldHandler( + (builder, name, parser, mapper) -> builder._custom(name, JsonData._DESERIALIZER.deserialize(parser, mapper)) + ); } public static final JsonpDeserializer _DESERIALIZER = ObjectBuilderDeserializer.lazy( @@ -1617,6 +1667,7 @@ public int hashCode() { int result = 17; result = 31 * result + Objects.hashCode(this._kind); result = 31 * result + Objects.hashCode(this._value); + result = 31 * result + Objects.hashCode(this._customKind); return result; } @@ -1625,6 +1676,43 @@ public boolean equals(Object o) { if (this == o) return true; if (o == null || this.getClass() != o.getClass()) return false; TokenFilterDefinition other = (TokenFilterDefinition) o; - return Objects.equals(this._kind, other._kind) && Objects.equals(this._value, other._value); + return Objects.equals(this._kind, other._kind) + && Objects.equals(this._value, other._value) + && Objects.equals(this._customKind, other._customKind); + } + + // Wrapper so JsonData fits the variant interface slot for custom/plugin types + private static final class CustomVariant implements TokenFilterDefinitionVariant, PlainJsonSerializable { + private final JsonData data; + + CustomVariant(JsonData data) { + this.data = data; + } + + public JsonData data() { + return data; + } + + @Override + public Kind _tokenFilterDefinitionKind() { + return Kind._Custom; + } + + @Override + public void serialize(JsonGenerator generator, JsonpMapper mapper) { + data.serialize(generator, mapper); + } + + @Override + public int hashCode() { + return data.hashCode(); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + return data.equals(((CustomVariant) o).data); + } } } diff --git a/java-client/src/generated/java/org/opensearch/client/opensearch/_types/analysis/TokenizerDefinition.java b/java-client/src/generated/java/org/opensearch/client/opensearch/_types/analysis/TokenizerDefinition.java index b999d3da57..18e37e6431 100644 --- a/java-client/src/generated/java/org/opensearch/client/opensearch/_types/analysis/TokenizerDefinition.java +++ b/java-client/src/generated/java/org/opensearch/client/opensearch/_types/analysis/TokenizerDefinition.java @@ -41,6 +41,7 @@ import java.util.function.Function; import javax.annotation.Generated; import javax.annotation.Nonnull; +import org.opensearch.client.json.JsonData; import org.opensearch.client.json.JsonEnum; import org.opensearch.client.json.JsonpDeserializable; import org.opensearch.client.json.JsonpDeserializer; @@ -79,7 +80,9 @@ public enum Kind implements JsonEnum { SmartcnTokenizer("smartcn_tokenizer"), Standard("standard"), UaxUrlEmail("uax_url_email"), - Whitespace("whitespace"); + Whitespace("whitespace"), + /** A custom variant type not natively supported by this client. */ + _Custom(null); private final String jsonValue; @@ -95,6 +98,7 @@ public String jsonValue() { private final Kind _kind; private final TokenizerDefinitionVariant _value; + private final String _customKind; @Override public final Kind _kind() { @@ -106,14 +110,23 @@ public final TokenizerDefinitionVariant _get() { return _value; } + /** + * Returns the actual type name when {@code _kind() == Kind._Custom}, otherwise {@code null}. + */ + public final String _customKind() { + return _customKind; + } + public TokenizerDefinition(TokenizerDefinitionVariant value) { this._kind = ApiTypeHelper.requireNonNull(value._tokenizerDefinitionKind(), this, ""); this._value = ApiTypeHelper.requireNonNull(value, this, ""); + this._customKind = null; } private TokenizerDefinition(Builder builder) { this._kind = ApiTypeHelper.requireNonNull(builder._kind, builder, ""); this._value = ApiTypeHelper.requireNonNull(builder._value, builder, ""); + this._customKind = builder._customKind; } public static TokenizerDefinition of(Function> fn) { @@ -392,6 +405,25 @@ public WhitespaceTokenizer whitespace() { return TaggedUnionUtils.get(this, Kind.Whitespace); } + /** + * Is this variant instance of kind {@code _custom}? + */ + public boolean _isCustom() { + return _kind == Kind._Custom; + } + + /** + * Get the raw JSON data for a custom (plugin-provided) variant type. + * + * @throws IllegalStateException if the current variant is not the {@code _custom} kind. + */ + public JsonData _custom() { + if (_kind != Kind._Custom) { + throw new IllegalStateException("Expected variant kind '_custom' but got '" + _kind + "'"); + } + return ((CustomVariant) _value).data(); + } + @Override public void serialize(JsonGenerator generator, JsonpMapper mapper) { mapper.serialize(_value, generator); @@ -410,12 +442,14 @@ public static Builder builder() { public static class Builder extends ObjectBuilderBase implements ObjectBuilder { private Kind _kind; private TokenizerDefinitionVariant _value; + private String _customKind; public Builder() {} private Builder(TokenizerDefinition o) { this._kind = o._kind; this._value = o._value; + this._customKind = o._customKind; } public ObjectBuilder charGroup(CharGroupTokenizer v) { @@ -598,6 +632,19 @@ public ObjectBuilder whitespace(Function _custom(String type, JsonData data) { + this._kind = Kind._Custom; + this._customKind = ApiTypeHelper.requireNonNull(type, this, ""); + this._value = new CustomVariant(ApiTypeHelper.requireNonNull(data, this, "")); + return this; + } + @Override public TokenizerDefinition build() { _checkSingleUse(); @@ -624,6 +671,9 @@ protected static void setupTokenizerDefinitionDeserializer(ObjectDeserializer builder._custom(name, JsonData._DESERIALIZER.deserialize(parser, mapper)) + ); } public static final JsonpDeserializer _DESERIALIZER = ObjectBuilderDeserializer.lazy( @@ -637,6 +687,7 @@ public int hashCode() { int result = 17; result = 31 * result + Objects.hashCode(this._kind); result = 31 * result + Objects.hashCode(this._value); + result = 31 * result + Objects.hashCode(this._customKind); return result; } @@ -645,6 +696,43 @@ public boolean equals(Object o) { if (this == o) return true; if (o == null || this.getClass() != o.getClass()) return false; TokenizerDefinition other = (TokenizerDefinition) o; - return Objects.equals(this._kind, other._kind) && Objects.equals(this._value, other._value); + return Objects.equals(this._kind, other._kind) + && Objects.equals(this._value, other._value) + && Objects.equals(this._customKind, other._customKind); + } + + // Wrapper so JsonData fits the variant interface slot for custom/plugin types + private static final class CustomVariant implements TokenizerDefinitionVariant, PlainJsonSerializable { + private final JsonData data; + + CustomVariant(JsonData data) { + this.data = data; + } + + public JsonData data() { + return data; + } + + @Override + public Kind _tokenizerDefinitionKind() { + return Kind._Custom; + } + + @Override + public void serialize(JsonGenerator generator, JsonpMapper mapper) { + data.serialize(generator, mapper); + } + + @Override + public int hashCode() { + return data.hashCode(); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + return data.equals(((CustomVariant) o).data); + } } } diff --git a/java-client/src/generated/java/org/opensearch/client/opensearch/_types/mapping/Property.java b/java-client/src/generated/java/org/opensearch/client/opensearch/_types/mapping/Property.java index 87a02aa2eb..09c8d20aa3 100644 --- a/java-client/src/generated/java/org/opensearch/client/opensearch/_types/mapping/Property.java +++ b/java-client/src/generated/java/org/opensearch/client/opensearch/_types/mapping/Property.java @@ -41,6 +41,7 @@ import java.util.function.Function; import javax.annotation.Generated; import javax.annotation.Nonnull; +import org.opensearch.client.json.JsonData; import org.opensearch.client.json.JsonEnum; import org.opensearch.client.json.JsonpDeserializable; import org.opensearch.client.json.JsonpDeserializer; @@ -109,7 +110,9 @@ public enum Kind implements JsonEnum { Version("version"), Wildcard("wildcard"), XyPoint("xy_point"), - XyShape("xy_shape"); + XyShape("xy_shape"), + /** A custom variant type not natively supported by this client. */ + _Custom(null); private final String jsonValue; @@ -125,6 +128,7 @@ public String jsonValue() { private final Kind _kind; private final PropertyVariant _value; + private final String _customKind; @Override public final Kind _kind() { @@ -136,14 +140,23 @@ public final PropertyVariant _get() { return _value; } + /** + * Returns the actual type name when {@code _kind() == Kind._Custom}, otherwise {@code null}. + */ + public final String _customKind() { + return _customKind; + } + public Property(PropertyVariant value) { this._kind = ApiTypeHelper.requireNonNull(value._propertyKind(), this, ""); this._value = ApiTypeHelper.requireNonNull(value, this, ""); + this._customKind = null; } private Property(Builder builder) { this._kind = ApiTypeHelper.requireNonNull(builder._kind, builder, ""); this._value = ApiTypeHelper.requireNonNull(builder._value, builder, ""); + this._customKind = builder._customKind; } public static Property of(Function> fn) { @@ -902,6 +915,25 @@ public XyShapeProperty xyShape() { return TaggedUnionUtils.get(this, Kind.XyShape); } + /** + * Is this variant instance of kind {@code _custom}? + */ + public boolean _isCustom() { + return _kind == Kind._Custom; + } + + /** + * Get the raw JSON data for a custom (plugin-provided) variant type. + * + * @throws IllegalStateException if the current variant is not the {@code _custom} kind. + */ + public JsonData _custom() { + if (_kind != Kind._Custom) { + throw new IllegalStateException("Expected variant kind '_custom' but got '" + _kind + "'"); + } + return ((CustomVariant) _value).data(); + } + @Override public void serialize(JsonGenerator generator, JsonpMapper mapper) { mapper.serialize(_value, generator); @@ -920,12 +952,14 @@ public static Builder builder() { public static class Builder extends ObjectBuilderBase implements ObjectBuilder { private Kind _kind; private PropertyVariant _value; + private String _customKind; public Builder() {} private Builder(Property o) { this._kind = o._kind; this._value = o._value; + this._customKind = o._customKind; } public ObjectBuilder aggregateMetricDouble(AggregateMetricDoubleProperty v) { @@ -1410,6 +1444,19 @@ public ObjectBuilder xyShape(Function _custom(String type, JsonData data) { + this._kind = Kind._Custom; + this._customKind = ApiTypeHelper.requireNonNull(type, this, ""); + this._value = new CustomVariant(ApiTypeHelper.requireNonNull(data, this, "")); + return this; + } + @Override public Property build() { _checkSingleUse(); @@ -1466,6 +1513,9 @@ protected static void setupPropertyDeserializer(ObjectDeserializer op) op.add(Builder::xyPoint, XyPointProperty._DESERIALIZER, "xy_point"); op.add(Builder::xyShape, XyShapeProperty._DESERIALIZER, "xy_shape"); op.setTypeProperty("type", Kind.Object.jsonValue()); + op.setUnknownFieldHandler( + (builder, name, parser, mapper) -> builder._custom(name, JsonData._DESERIALIZER.deserialize(parser, mapper)) + ); } public static final JsonpDeserializer _DESERIALIZER = ObjectBuilderDeserializer.lazy( @@ -1479,6 +1529,7 @@ public int hashCode() { int result = 17; result = 31 * result + Objects.hashCode(this._kind); result = 31 * result + Objects.hashCode(this._value); + result = 31 * result + Objects.hashCode(this._customKind); return result; } @@ -1487,6 +1538,43 @@ public boolean equals(Object o) { if (this == o) return true; if (o == null || this.getClass() != o.getClass()) return false; Property other = (Property) o; - return Objects.equals(this._kind, other._kind) && Objects.equals(this._value, other._value); + return Objects.equals(this._kind, other._kind) + && Objects.equals(this._value, other._value) + && Objects.equals(this._customKind, other._customKind); + } + + // Wrapper so JsonData fits the variant interface slot for custom/plugin types + private static final class CustomVariant implements PropertyVariant, PlainJsonSerializable { + private final JsonData data; + + CustomVariant(JsonData data) { + this.data = data; + } + + public JsonData data() { + return data; + } + + @Override + public Kind _propertyKind() { + return Kind._Custom; + } + + @Override + public void serialize(JsonGenerator generator, JsonpMapper mapper) { + data.serialize(generator, mapper); + } + + @Override + public int hashCode() { + return data.hashCode(); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + return data.equals(((CustomVariant) o).data); + } } } diff --git a/java-client/src/generated/java/org/opensearch/client/opensearch/_types/query_dsl/Query.java b/java-client/src/generated/java/org/opensearch/client/opensearch/_types/query_dsl/Query.java index c3255666c1..aa99b5ce05 100644 --- a/java-client/src/generated/java/org/opensearch/client/opensearch/_types/query_dsl/Query.java +++ b/java-client/src/generated/java/org/opensearch/client/opensearch/_types/query_dsl/Query.java @@ -125,7 +125,9 @@ public enum Kind implements JsonEnum { Type("type"), Wildcard("wildcard"), Wrapper("wrapper"), - XyShape("xy_shape"); + XyShape("xy_shape"), + /** A custom variant type not natively supported by this client. */ + _Custom(null); private final String jsonValue; @@ -149,6 +151,7 @@ public Aggregation.Kind _aggregationKind() { private final Kind _kind; private final Object _value; + private final String _customKind; @Override public final Kind _kind() { @@ -160,14 +163,23 @@ public final Object _get() { return _value; } + /** + * Returns the actual type name when {@code _kind() == Kind._Custom}, otherwise {@code null}. + */ + public final String _customKind() { + return _customKind; + } + public Query(QueryVariant value) { this._kind = ApiTypeHelper.requireNonNull(value._queryKind(), this, ""); this._value = ApiTypeHelper.requireNonNull(value, this, ""); + this._customKind = null; } private Query(Builder builder) { this._kind = ApiTypeHelper.requireNonNull(builder._kind, builder, ""); this._value = ApiTypeHelper.requireNonNull(builder._value, builder, ""); + this._customKind = builder._customKind; } public static Query of(Function> fn) { @@ -1102,10 +1114,29 @@ public XyShapeQuery xyShape() { return TaggedUnionUtils.get(this, Kind.XyShape); } + /** + * Is this variant instance of kind {@code _custom}? + */ + public boolean _isCustom() { + return _kind == Kind._Custom; + } + + /** + * Get the raw JSON data for a custom (plugin-provided) variant type. + * + * @throws IllegalStateException if the current variant is not the {@code _custom} kind. + */ + public JsonData _custom() { + if (_kind != Kind._Custom) { + throw new IllegalStateException("Expected variant kind '_custom' but got '" + _kind + "'"); + } + return ((CustomVariant) _value).data(); + } + @Override public void serialize(JsonGenerator generator, JsonpMapper mapper) { generator.writeStartObject(); - generator.writeKey(_kind.jsonValue()); + generator.writeKey(_kind == Kind._Custom ? _customKind : _kind.jsonValue()); if (_value instanceof JsonpSerializable) { ((JsonpSerializable) _value).serialize(generator, mapper); } else { @@ -1136,12 +1167,14 @@ public static Builder builder() { public static class Builder extends ObjectBuilderBase implements ObjectBuilder { private Kind _kind; private Object _value; + private String _customKind; public Builder() {} private Builder(Query o) { this._kind = o._kind; this._value = o._value; + this._customKind = o._customKind; } public ObjectBuilder agentic(AgenticQuery v) { @@ -1720,6 +1753,19 @@ public ObjectBuilder xyShape(Function _custom(String type, JsonData data) { + this._kind = Kind._Custom; + this._customKind = ApiTypeHelper.requireNonNull(type, this, ""); + this._value = new CustomVariant(ApiTypeHelper.requireNonNull(data, this, "")); + return this; + } + @Override public Query build() { _checkSingleUse(); @@ -1786,6 +1832,9 @@ protected static void setupQueryDeserializer(ObjectDeserializer op) { op.add(Builder::wildcard, WildcardQuery._DESERIALIZER, "wildcard"); op.add(Builder::wrapper, WrapperQuery._DESERIALIZER, "wrapper"); op.add(Builder::xyShape, XyShapeQuery._DESERIALIZER, "xy_shape"); + op.setUnknownFieldHandler( + (builder, name, parser, mapper) -> builder._custom(name, JsonData._DESERIALIZER.deserialize(parser, mapper)) + ); } public static final JsonpDeserializer _DESERIALIZER = ObjectBuilderDeserializer.lazy( @@ -1799,6 +1848,7 @@ public int hashCode() { int result = 17; result = 31 * result + Objects.hashCode(this._kind); result = 31 * result + Objects.hashCode(this._value); + result = 31 * result + Objects.hashCode(this._customKind); return result; } @@ -1807,6 +1857,43 @@ public boolean equals(Object o) { if (this == o) return true; if (o == null || this.getClass() != o.getClass()) return false; Query other = (Query) o; - return Objects.equals(this._kind, other._kind) && Objects.equals(this._value, other._value); + return Objects.equals(this._kind, other._kind) + && Objects.equals(this._value, other._value) + && Objects.equals(this._customKind, other._customKind); + } + + // Wrapper so JsonData fits the variant interface slot for custom/plugin types + private static final class CustomVariant implements QueryVariant, PlainJsonSerializable { + private final JsonData data; + + CustomVariant(JsonData data) { + this.data = data; + } + + public JsonData data() { + return data; + } + + @Override + public Kind _queryKind() { + return Kind._Custom; + } + + @Override + public void serialize(JsonGenerator generator, JsonpMapper mapper) { + data.serialize(generator, mapper); + } + + @Override + public int hashCode() { + return data.hashCode(); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + return data.equals(((CustomVariant) o).data); + } } } diff --git a/java-client/src/generated/java/org/opensearch/client/opensearch/_types/query_dsl/SpanQuery.java b/java-client/src/generated/java/org/opensearch/client/opensearch/_types/query_dsl/SpanQuery.java index 285a353e14..381cf101e9 100644 --- a/java-client/src/generated/java/org/opensearch/client/opensearch/_types/query_dsl/SpanQuery.java +++ b/java-client/src/generated/java/org/opensearch/client/opensearch/_types/query_dsl/SpanQuery.java @@ -41,6 +41,7 @@ import java.util.function.Function; import javax.annotation.Generated; import javax.annotation.Nonnull; +import org.opensearch.client.json.JsonData; import org.opensearch.client.json.JsonEnum; import org.opensearch.client.json.JsonpDeserializable; import org.opensearch.client.json.JsonpDeserializer; @@ -73,7 +74,9 @@ public enum Kind implements JsonEnum { SpanNot("span_not"), SpanOr("span_or"), SpanTerm("span_term"), - SpanWithin("span_within"); + SpanWithin("span_within"), + /** A custom variant type not natively supported by this client. */ + _Custom(null); private final String jsonValue; @@ -89,6 +92,7 @@ public String jsonValue() { private final Kind _kind; private final SpanQueryVariant _value; + private final String _customKind; @Override public final Kind _kind() { @@ -100,14 +104,23 @@ public final SpanQueryVariant _get() { return _value; } + /** + * Returns the actual type name when {@code _kind() == Kind._Custom}, otherwise {@code null}. + */ + public final String _customKind() { + return _customKind; + } + public SpanQuery(SpanQueryVariant value) { this._kind = ApiTypeHelper.requireNonNull(value._spanQueryKind(), this, ""); this._value = ApiTypeHelper.requireNonNull(value, this, ""); + this._customKind = null; } private SpanQuery(Builder builder) { this._kind = ApiTypeHelper.requireNonNull(builder._kind, builder, ""); this._value = ApiTypeHelper.requireNonNull(builder._value, builder, ""); + this._customKind = builder._customKind; } public static SpanQuery of(Function> fn) { @@ -274,10 +287,29 @@ public SpanWithinQuery spanWithin() { return TaggedUnionUtils.get(this, Kind.SpanWithin); } + /** + * Is this variant instance of kind {@code _custom}? + */ + public boolean _isCustom() { + return _kind == Kind._Custom; + } + + /** + * Get the raw JSON data for a custom (plugin-provided) variant type. + * + * @throws IllegalStateException if the current variant is not the {@code _custom} kind. + */ + public JsonData _custom() { + if (_kind != Kind._Custom) { + throw new IllegalStateException("Expected variant kind '_custom' but got '" + _kind + "'"); + } + return ((CustomVariant) _value).data(); + } + @Override public void serialize(JsonGenerator generator, JsonpMapper mapper) { generator.writeStartObject(); - generator.writeKey(_kind.jsonValue()); + generator.writeKey(_kind == Kind._Custom ? _customKind : _kind.jsonValue()); if (_value instanceof JsonpSerializable) { ((JsonpSerializable) _value).serialize(generator, mapper); } @@ -297,12 +329,14 @@ public static Builder builder() { public static class Builder extends ObjectBuilderBase implements ObjectBuilder { private Kind _kind; private SpanQueryVariant _value; + private String _customKind; public Builder() {} private Builder(SpanQuery o) { this._kind = o._kind; this._value = o._value; + this._customKind = o._customKind; } public ObjectBuilder fieldMaskingSpan(SpanFieldMaskingQuery v) { @@ -405,6 +439,19 @@ public ObjectBuilder spanWithin(Function _custom(String type, JsonData data) { + this._kind = Kind._Custom; + this._customKind = ApiTypeHelper.requireNonNull(type, this, ""); + this._value = new CustomVariant(ApiTypeHelper.requireNonNull(data, this, "")); + return this; + } + @Override public SpanQuery build() { _checkSingleUse(); @@ -423,6 +470,9 @@ protected static void setupSpanQueryDeserializer(ObjectDeserializer op) op.add(Builder::spanOr, SpanOrQuery._DESERIALIZER, "span_or"); op.add(Builder::spanTerm, SpanTermQuery._DESERIALIZER, "span_term"); op.add(Builder::spanWithin, SpanWithinQuery._DESERIALIZER, "span_within"); + op.setUnknownFieldHandler( + (builder, name, parser, mapper) -> builder._custom(name, JsonData._DESERIALIZER.deserialize(parser, mapper)) + ); } public static final JsonpDeserializer _DESERIALIZER = ObjectBuilderDeserializer.lazy( @@ -436,6 +486,7 @@ public int hashCode() { int result = 17; result = 31 * result + Objects.hashCode(this._kind); result = 31 * result + Objects.hashCode(this._value); + result = 31 * result + Objects.hashCode(this._customKind); return result; } @@ -444,6 +495,43 @@ public boolean equals(Object o) { if (this == o) return true; if (o == null || this.getClass() != o.getClass()) return false; SpanQuery other = (SpanQuery) o; - return Objects.equals(this._kind, other._kind) && Objects.equals(this._value, other._value); + return Objects.equals(this._kind, other._kind) + && Objects.equals(this._value, other._value) + && Objects.equals(this._customKind, other._customKind); + } + + // Wrapper so JsonData fits the variant interface slot for custom/plugin types + private static final class CustomVariant implements SpanQueryVariant, PlainJsonSerializable { + private final JsonData data; + + CustomVariant(JsonData data) { + this.data = data; + } + + public JsonData data() { + return data; + } + + @Override + public Kind _spanQueryKind() { + return Kind._Custom; + } + + @Override + public void serialize(JsonGenerator generator, JsonpMapper mapper) { + data.serialize(generator, mapper); + } + + @Override + public int hashCode() { + return data.hashCode(); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + return data.equals(((CustomVariant) o).data); + } } } diff --git a/java-client/src/generated/java/org/opensearch/client/opensearch/ingest/Processor.java b/java-client/src/generated/java/org/opensearch/client/opensearch/ingest/Processor.java index f016292223..134a2a9bab 100644 --- a/java-client/src/generated/java/org/opensearch/client/opensearch/ingest/Processor.java +++ b/java-client/src/generated/java/org/opensearch/client/opensearch/ingest/Processor.java @@ -41,6 +41,7 @@ import java.util.function.Function; import javax.annotation.Generated; import javax.annotation.Nonnull; +import org.opensearch.client.json.JsonData; import org.opensearch.client.json.JsonEnum; import org.opensearch.client.json.JsonpDeserializable; import org.opensearch.client.json.JsonpDeserializer; @@ -97,7 +98,9 @@ public enum Kind implements JsonEnum { Trim("trim"), Uppercase("uppercase"), Urldecode("urldecode"), - UserAgent("user_agent"); + UserAgent("user_agent"), + /** A custom variant type not natively supported by this client. */ + _Custom(null); private final String jsonValue; @@ -113,6 +116,7 @@ public String jsonValue() { private final Kind _kind; private final ProcessorVariant _value; + private final String _customKind; @Override public final Kind _kind() { @@ -124,14 +128,23 @@ public final ProcessorVariant _get() { return _value; } + /** + * Returns the actual type name when {@code _kind() == Kind._Custom}, otherwise {@code null}. + */ + public final String _customKind() { + return _customKind; + } + public Processor(ProcessorVariant value) { this._kind = ApiTypeHelper.requireNonNull(value._processorKind(), this, ""); this._value = ApiTypeHelper.requireNonNull(value, this, ""); + this._customKind = null; } private Processor(Builder builder) { this._kind = ApiTypeHelper.requireNonNull(builder._kind, builder, ""); this._value = ApiTypeHelper.requireNonNull(builder._value, builder, ""); + this._customKind = builder._customKind; } public static Processor of(Function> fn) { @@ -666,10 +679,29 @@ public UserAgentProcessor userAgent() { return TaggedUnionUtils.get(this, Kind.UserAgent); } + /** + * Is this variant instance of kind {@code _custom}? + */ + public boolean _isCustom() { + return _kind == Kind._Custom; + } + + /** + * Get the raw JSON data for a custom (plugin-provided) variant type. + * + * @throws IllegalStateException if the current variant is not the {@code _custom} kind. + */ + public JsonData _custom() { + if (_kind != Kind._Custom) { + throw new IllegalStateException("Expected variant kind '_custom' but got '" + _kind + "'"); + } + return ((CustomVariant) _value).data(); + } + @Override public void serialize(JsonGenerator generator, JsonpMapper mapper) { generator.writeStartObject(); - generator.writeKey(_kind.jsonValue()); + generator.writeKey(_kind == Kind._Custom ? _customKind : _kind.jsonValue()); if (_value instanceof JsonpSerializable) { ((JsonpSerializable) _value).serialize(generator, mapper); } @@ -689,12 +721,14 @@ public static Builder builder() { public static class Builder extends ObjectBuilderBase implements ObjectBuilder { private Kind _kind; private ProcessorVariant _value; + private String _customKind; public Builder() {} private Builder(Processor o) { this._kind = o._kind; this._value = o._value; + this._customKind = o._customKind; } public ObjectBuilder append(AppendProcessor v) { @@ -1029,6 +1063,19 @@ public ObjectBuilder userAgent(Function _custom(String type, JsonData data) { + this._kind = Kind._Custom; + this._customKind = ApiTypeHelper.requireNonNull(type, this, ""); + this._value = new CustomVariant(ApiTypeHelper.requireNonNull(data, this, "")); + return this; + } + @Override public Processor build() { _checkSingleUse(); @@ -1070,6 +1117,9 @@ protected static void setupProcessorDeserializer(ObjectDeserializer op) op.add(Builder::uppercase, UppercaseProcessor._DESERIALIZER, "uppercase"); op.add(Builder::urldecode, UrlDecodeProcessor._DESERIALIZER, "urldecode"); op.add(Builder::userAgent, UserAgentProcessor._DESERIALIZER, "user_agent"); + op.setUnknownFieldHandler( + (builder, name, parser, mapper) -> builder._custom(name, JsonData._DESERIALIZER.deserialize(parser, mapper)) + ); } public static final JsonpDeserializer _DESERIALIZER = ObjectBuilderDeserializer.lazy( @@ -1083,6 +1133,7 @@ public int hashCode() { int result = 17; result = 31 * result + Objects.hashCode(this._kind); result = 31 * result + Objects.hashCode(this._value); + result = 31 * result + Objects.hashCode(this._customKind); return result; } @@ -1091,6 +1142,43 @@ public boolean equals(Object o) { if (this == o) return true; if (o == null || this.getClass() != o.getClass()) return false; Processor other = (Processor) o; - return Objects.equals(this._kind, other._kind) && Objects.equals(this._value, other._value); + return Objects.equals(this._kind, other._kind) + && Objects.equals(this._value, other._value) + && Objects.equals(this._customKind, other._customKind); + } + + // Wrapper so JsonData fits the variant interface slot for custom/plugin types + private static final class CustomVariant implements ProcessorVariant, PlainJsonSerializable { + private final JsonData data; + + CustomVariant(JsonData data) { + this.data = data; + } + + public JsonData data() { + return data; + } + + @Override + public Kind _processorKind() { + return Kind._Custom; + } + + @Override + public void serialize(JsonGenerator generator, JsonpMapper mapper) { + data.serialize(generator, mapper); + } + + @Override + public int hashCode() { + return data.hashCode(); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + return data.equals(((CustomVariant) o).data); + } } } diff --git a/java-client/src/main/java/org/opensearch/client/json/ExternallyTaggedUnion.java b/java-client/src/main/java/org/opensearch/client/json/ExternallyTaggedUnion.java index f8beafceb7..7327412a9b 100644 --- a/java-client/src/main/java/org/opensearch/client/json/ExternallyTaggedUnion.java +++ b/java-client/src/main/java/org/opensearch/client/json/ExternallyTaggedUnion.java @@ -89,6 +89,7 @@ public Union deserialize(String type, JsonParser parser, JsonpMapper mapper, Eve if (unKnownUnionCtor != null) { return unKnownUnionCtor.apply(type, JsonData._DESERIALIZER.deserialize(parser, mapper, event)); } + throw new jakarta.json.JsonException("Unknown variant type '" + type + "'"); } return unionCtor.apply(deserializer.deserialize(parser, mapper, event)); @@ -191,7 +192,11 @@ public void deserializeEntry(String key, JsonParser parser, JsonpMapper mapper, if (mapper.attribute(JsonpMapperAttributes.SERIALIZE_TYPED_KEYS, true)) { for (Map.Entry entry : map.entrySet()) { T value = entry.getValue(); - generator.writeKey(value._kind().jsonValue() + "#" + entry.getKey()); + String type = value._kind().jsonValue(); + if (type == null) { + type = value._customKind(); + } + generator.writeKey(type + "#" + entry.getKey()); value.serialize(generator, mapper); } } else { @@ -223,7 +228,11 @@ public void deserializeEntry(String key, JsonParser parser, JsonpMapper mapper, if (list.isEmpty()) { continue; } - generator.writeKey(list.get(0)._kind().jsonValue() + "#" + entry.getKey()); + String type = list.get(0)._kind().jsonValue(); + if (type == null) { + type = list.get(0)._customKind(); + } + generator.writeKey(type + "#" + entry.getKey()); generator.writeStartArray(); for (T value : list) { value.serialize(generator, mapper); diff --git a/java-client/src/main/java/org/opensearch/client/util/TaggedUnion.java b/java-client/src/main/java/org/opensearch/client/util/TaggedUnion.java index bc4f048b3e..e0b3284d4a 100644 --- a/java-client/src/main/java/org/opensearch/client/util/TaggedUnion.java +++ b/java-client/src/main/java/org/opensearch/client/util/TaggedUnion.java @@ -42,4 +42,12 @@ public interface TaggedUnion, BaseType> { Tag _kind(); BaseType _get(); + + /** + * Returns the actual type name for custom (plugin-provided) variant kinds whose + * {@code jsonValue} is {@code null}. Returns {@code null} for all built-in kinds. + */ + default String _customKind() { + return null; + } } diff --git a/java-client/src/test/java/org/opensearch/client/opensearch/model/CustomVariantTest.java b/java-client/src/test/java/org/opensearch/client/opensearch/model/CustomVariantTest.java new file mode 100644 index 0000000000..91925f3fc4 --- /dev/null +++ b/java-client/src/test/java/org/opensearch/client/opensearch/model/CustomVariantTest.java @@ -0,0 +1,134 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + */ + +package org.opensearch.client.opensearch.model; + +import jakarta.json.stream.JsonParser; +import java.io.StringReader; +import java.util.Map; +import org.junit.Test; +import org.opensearch.client.json.JsonData; +import org.opensearch.client.json.JsonpDeserializer; +import org.opensearch.client.opensearch._types.aggregations.Aggregate; +import org.opensearch.client.opensearch._types.query_dsl.Query; +import org.opensearch.client.opensearch.core.SearchResponse; + +/** + * Tests that unknown/plugin-provided aggregation types deserialize into the {@code _Custom} variant + * instead of throwing or silently dropping data. + */ +public class CustomVariantTest extends ModelTestCase { + + @Test + public void testUnknownAggregationTypeDeserializesToCustomVariant() { + // A response containing a fictional "my_plugin_agg" typed-key aggregation + String json = "{\"took\":1,\"timed_out\":false," + + "\"_shards\":{\"total\":1,\"successful\":1,\"skipped\":0,\"failed\":0}," + + "\"hits\":{\"total\":{\"value\":0,\"relation\":\"eq\"},\"hits\":[]}," + + "\"aggregations\":{\"my_plugin_agg#foo\":{\"custom_field\":42,\"label\":\"hello\"}}}"; + + JsonParser parser = mapper.jsonProvider().createParser(new StringReader(json)); + SearchResponse response = SearchResponse._DESERIALIZER.deserialize(parser, mapper); + + Map aggs = response.aggregations(); + assertTrue("aggregation 'foo' should be present", aggs.containsKey("foo")); + + Aggregate agg = aggs.get("foo"); + assertEquals(Aggregate.Kind._Custom, agg._kind()); + assertTrue(agg._isCustom()); + assertEquals("my_plugin_agg", agg._customKind()); + + // The raw data is accessible and contains the original payload + JsonData data = agg._custom(); + assertNotNull(data); + assertEquals(42, data.toJson().asJsonObject().getInt("custom_field")); + assertEquals("hello", data.toJson().asJsonObject().getString("label")); + } + + @Test + public void testCustomVariantSerializesRoundTrip() { + // Build a custom variant programmatically + Aggregate agg = new Aggregate.Builder()._custom("my_plugin_agg", JsonData.of(Map.of("x", 1))).build(); + + assertTrue(agg._isCustom()); + assertEquals("my_plugin_agg", agg._customKind()); + + // Serialize and verify it produces JSON + String json = toJson(agg); + assertNotNull(json); + assertTrue("serialized JSON should contain the payload", json.contains("\"x\"")); + } + + @Test + public void testKnownAggregationTypeStillWorks() { + // Sanity: a known type (avg) still deserializes normally + String json = "{\"took\":1,\"timed_out\":false," + + "\"_shards\":{\"total\":1,\"successful\":1,\"skipped\":0,\"failed\":0}," + + "\"hits\":{\"total\":{\"value\":0,\"relation\":\"eq\"},\"hits\":[]}," + + "\"aggregations\":{\"avg#bar\":{\"value\":3.14}}}"; + + SearchResponse response = fromJson( + json, + SearchResponse.createSearchResponseDeserializer(JsonpDeserializer.of(Object.class)) + ); + + Aggregate agg = response.aggregations().get("bar"); + assertEquals(Aggregate.Kind.Avg, agg._kind()); + assertFalse(agg._isCustom()); + assertNull(agg._customKind()); + assertEquals(3.14, agg.avg().value(), 0.001); + } + + @Test + public void testCustomAggregateTypedKeysRoundTrip() { + // Simulates a typed-keys response with a custom aggregation, then re-serializes + String json = "{\"took\":1,\"timed_out\":false," + + "\"_shards\":{\"total\":1,\"successful\":1,\"skipped\":0,\"failed\":0}," + + "\"hits\":{\"total\":{\"value\":0,\"relation\":\"eq\"},\"hits\":[]}," + + "\"aggregations\":{\"my_plugin_agg#metric\":{\"score\":99}}}"; + + SearchResponse response = fromJson( + json, + SearchResponse.createSearchResponseDeserializer(JsonpDeserializer.of(Object.class)) + ); + + // Round-trip: serialize the response back to JSON + String reserialized = toJson(response); + // The typed-key format must use the custom type name, not "null" + assertTrue("typed-key must contain custom type name", reserialized.contains("my_plugin_agg#metric")); + assertTrue("payload must survive round-trip", reserialized.contains("\"score\"")); + } + + @Test + public void testCustomQueryVariantRoundTrip() { + // An internally-tagged query with a plugin-provided type + String json = "{\"my_plugin_query\":{\"param\":\"value\"}}"; + + Query query = fromJson(json, Query._DESERIALIZER); + + assertTrue(query._isCustom()); + assertEquals("my_plugin_query", query._customKind()); + assertNotNull(query._custom()); + + // Round-trip serialization + String reserialized = toJson(query); + assertTrue("must contain custom type key", reserialized.contains("\"my_plugin_query\"")); + assertTrue("must contain payload", reserialized.contains("\"param\"")); + } + + @Test + public void testCustomVariantEquality() { + Aggregate a = new Aggregate.Builder()._custom("type_a", JsonData.of(Map.of("x", 1))).build(); + Aggregate b = new Aggregate.Builder()._custom("type_a", JsonData.of(Map.of("x", 1))).build(); + Aggregate c = new Aggregate.Builder()._custom("type_b", JsonData.of(Map.of("x", 1))).build(); + + assertEquals(a, b); + assertEquals(a.hashCode(), b.hashCode()); + assertNotEquals(a, c); // different _customKind + } +} diff --git a/java-codegen/opensearch-openapi.yaml b/java-codegen/opensearch-openapi.yaml index b4829f72a0..d1c98fbd42 100644 --- a/java-codegen/opensearch-openapi.yaml +++ b/java-codegen/opensearch-openapi.yaml @@ -41491,6 +41491,7 @@ components: - key _common.aggregations___Aggregate: x-supports-typed-keys: true + x-non-exhaustive: true anyOf: - title: adjacency_matrix $ref: '#/components/schemas/_common.aggregations___AdjacencyMatrixAggregate' @@ -41641,6 +41642,7 @@ components: name: type: string _common.aggregations___AggregationContainer: + x-non-exhaustive: true allOf: - type: object properties: @@ -44912,6 +44914,7 @@ components: - type: object _common.analysis___Analyzer: type: object + x-non-exhaustive: true discriminator: propertyName: type oneOf: @@ -44959,6 +44962,7 @@ components: $ref: '#/components/schemas/_common___VersionString' _common.analysis___CharFilterDefinition: type: object + x-non-exhaustive: true discriminator: propertyName: type oneOf: @@ -46531,6 +46535,7 @@ components: $ref: '#/components/schemas/_common___VersionString' _common.analysis___TokenFilterDefinition: type: object + x-non-exhaustive: true discriminator: propertyName: type oneOf: @@ -46597,6 +46602,7 @@ components: $ref: '#/components/schemas/_common___VersionString' _common.analysis___TokenizerDefinition: type: object + x-non-exhaustive: true discriminator: propertyName: type oneOf: @@ -47592,6 +47598,7 @@ components: - type _common.mapping___Property: type: object + x-non-exhaustive: true discriminator: propertyName: type x-default: object @@ -49429,6 +49436,7 @@ components: type: string _common.query_dsl___QueryContainer: type: object + x-non-exhaustive: true properties: agentic: $ref: '#/components/schemas/_common.query_dsl___AgenticQuery' @@ -50106,6 +50114,7 @@ components: - clauses _common.query_dsl___SpanQuery: type: object + x-non-exhaustive: true properties: span_containing: $ref: '#/components/schemas/_common.query_dsl___SpanContainingQuery' @@ -51750,6 +51759,7 @@ components: required: - field _core.search___FieldSuggester: + x-non-exhaustive: true allOf: - type: object properties: @@ -60732,6 +60742,7 @@ components: type: string ingest._common___ProcessorContainer: type: object + x-non-exhaustive: true properties: attachment: $ref: '#/components/schemas/ingest._common___AttachmentProcessor' diff --git a/java-codegen/src/main/java/org/opensearch/client/codegen/model/TaggedUnionShape.java b/java-codegen/src/main/java/org/opensearch/client/codegen/model/TaggedUnionShape.java index 924afb3d89..ddef31d4f9 100644 --- a/java-codegen/src/main/java/org/opensearch/client/codegen/model/TaggedUnionShape.java +++ b/java-codegen/src/main/java/org/opensearch/client/codegen/model/TaggedUnionShape.java @@ -32,6 +32,7 @@ public class TaggedUnionShape extends ObjectShapeBase { private String discriminatingField; private String defaultVariant; private ExternallyDiscriminated externallyDiscriminated; + private boolean nonExhaustive; public TaggedUnionShape(Namespace parent, String className, String typedefName, String description, ShouldGenerate shouldGenerate) { super(parent, className, typedefName, description, shouldGenerate); @@ -105,6 +106,14 @@ public void setExternallyDiscriminated(ExternallyDiscriminated externallyDiscrim this.externallyDiscriminated = externallyDiscriminated; } + public boolean isNonExhaustive() { + return nonExhaustive; + } + + public void setNonExhaustive(boolean nonExhaustive) { + this.nonExhaustive = nonExhaustive; + } + public Type getVariantBaseType() { return isDiscriminated() && getVariants().stream().map(Variant::getType).allMatch(t -> t.getTargetShape().isPresent()) ? getVariantInterfaceType() @@ -158,6 +167,9 @@ public Collection getHashableFields() { var fields = new ArrayList(); fields.add(Field.builder().withName("_kind", true).withType(getMaterializedType().getNestedType("Kind")).build()); fields.add(Field.builder().withName("_value", true).withType(getVariantBaseType()).build()); + if (isNonExhaustive()) { + fields.add(Field.builder().withName("_customKind", true).withType(Types.Java.Lang.String).build()); + } fields.addAll(super.getHashableFields()); return fields; } diff --git a/java-codegen/src/main/java/org/opensearch/client/codegen/openapi/OpenApiSchema.java b/java-codegen/src/main/java/org/opensearch/client/codegen/openapi/OpenApiSchema.java index 12046a6ee1..5b231a3b0e 100644 --- a/java-codegen/src/main/java/org/opensearch/client/codegen/openapi/OpenApiSchema.java +++ b/java-codegen/src/main/java/org/opensearch/client/codegen/openapi/OpenApiSchema.java @@ -100,6 +100,8 @@ public static OpenApiSchema string() { @Nullable private final Boolean supportsTypedKeys; @Nullable + private final Boolean nonExhaustive; + @Nullable private Boolean isGenericTypeParameter; @Nullable private OpenApiSchema $extends; @@ -131,6 +133,7 @@ private OpenApiSchema(@Nonnull Builder builder) { versionRemoved = builder.versionRemoved; versionDeprecated = builder.versionDeprecated; supportsTypedKeys = builder.supportsTypedKeys; + nonExhaustive = builder.nonExhaustive; isGenericTypeParameter = builder.isGenericTypeParameter; set$extends(builder.$extends); } @@ -180,6 +183,7 @@ private OpenApiSchema(@Nonnull Builder builder) { versionDeprecated = Maps.tryGet(extensions, "x-version-deprecated").map(v -> Versions.coerce((String) v)).orElse(null); supportsTypedKeys = Maps.tryGet(extensions, "x-supports-typed-keys").map(Boolean.class::cast).orElse(null); + nonExhaustive = Maps.tryGet(extensions, "x-non-exhaustive").map(Boolean.class::cast).orElse(null); isGenericTypeParameter = Maps.tryGet(extensions, "x-is-generic-type-parameter").map(Boolean.class::cast).orElse(null); } @@ -494,6 +498,10 @@ public boolean supportsTypedKeys() { return supportsTypedKeys != null && supportsTypedKeys; } + public boolean isNonExhaustive() { + return nonExhaustive != null && nonExhaustive; + } + public boolean isGenericTypeParameter() { return isGenericTypeParameter != null && isGenericTypeParameter; } @@ -792,6 +800,7 @@ protected void toJsonInner(JsonGenerator generator) { .withVersionRemoved(versionRemoved) .withVersionDeprecated(versionDeprecated) .withSupportsTypedKeys(supportsTypedKeys) + .withNonExhaustive(nonExhaustive) .withIsGenericTypeParameter(isGenericTypeParameter); } @@ -848,6 +857,8 @@ public static final class Builder extends OpenApiRefElement.AbstractBuilder props.forEach((k, v) -> taggedUnion.addVariant(k, typeMapper.mapType(v)))); } } + + taggedUnion.setNonExhaustive(unionSchema.isNonExhaustive()); } else if (schema.isShortcutPropertyObject() || schema.getSingleType().orElse(null) == OpenApiSchemaType.Object) { if (!schema.has$extends() && !schema.hasProperties() diff --git a/java-codegen/src/main/resources/org/opensearch/client/codegen/templates/TaggedUnionShape.mustache b/java-codegen/src/main/resources/org/opensearch/client/codegen/templates/TaggedUnionShape.mustache index 1b45c88676..57b022c57c 100644 --- a/java-codegen/src/main/resources/org/opensearch/client/codegen/templates/TaggedUnionShape.mustache +++ b/java-codegen/src/main/resources/org/opensearch/client/codegen/templates/TaggedUnionShape.mustache @@ -3,7 +3,9 @@ * {@link {{className}}} variant kinds. */ public enum Kind{{#discriminated}} implements {{TYPES.Client.Json.JsonEnum}}{{/discriminated}} { - {{#variants}}{{#pascalCase}}{{name}}{{/pascalCase}}{{#discriminated}}({{#quoted}}{{name}}{{/quoted}}){{/discriminated}}{{^-last}},{{/-last}}{{/variants}} + {{#variants}}{{#pascalCase}}{{name}}{{/pascalCase}}{{#discriminated}}({{#quoted}}{{name}}{{/quoted}}){{/discriminated}}{{^-last}},{{/-last}}{{/variants}}{{#nonExhaustive}}{{#discriminated}}, + /** A custom variant type not natively supported by this client. */ + _Custom(null){{/discriminated}}{{/nonExhaustive}} {{#discriminated}}; private final String jsonValue; @@ -25,6 +27,9 @@ private final Kind _kind; private final {{variantBaseType}} _value; +{{#nonExhaustive}} + private final String _customKind; +{{/nonExhaustive}} @Override public final Kind _kind() { @@ -35,6 +40,15 @@ public final {{variantBaseType}} _get() { return _value; } +{{#nonExhaustive}} + + /** + * Returns the actual type name when {@code _kind() == Kind._Custom}, otherwise {@code null}. + */ + public final String _customKind() { + return _customKind; + } +{{/nonExhaustive}} {{#hasFields}} {{>ObjectShape/Fields}} @@ -54,6 +68,9 @@ this._value = null; } {{/isOptionalExternallyDiscriminated}} + {{#nonExhaustive}} + this._customKind = null; + {{/nonExhaustive}} {{#fields}} this.{{name}} = null; {{/fields}} @@ -84,6 +101,9 @@ this._value = null; } {{/isOptionalExternallyDiscriminated}} + {{#nonExhaustive}} + this._customKind = builder._customKind; + {{/nonExhaustive}} } public static{{#typeParameters}} {{.}}{{/typeParameters}} {{selfType}} of({{selfType.builderFnType}} fn) { @@ -125,6 +145,27 @@ } {{/variants}} +{{#nonExhaustive}} + + /** + * Is this variant instance of kind {@code _custom}? + */ + public boolean _isCustom() { + return _kind == Kind._Custom; + } + + /** + * Get the raw JSON data for a custom (plugin-provided) variant type. + * + * @throws IllegalStateException if the current variant is not the {@code _custom} kind. + */ + public {{TYPES.Client.Json.JsonData}} _custom() { + if (_kind != Kind._Custom) { + throw new IllegalStateException("Expected variant kind '_custom' but got '" + _kind + "'"); + } + return ((CustomVariant) _value).data(); + } +{{/nonExhaustive}} {{>TaggedUnionShape/Serialize}} @@ -138,6 +179,9 @@ public static class Builder{{#typeParameters}}{{.}}{{/typeParameters}} extends {{#extendsOtherShape}}{{extendsType}}.AbstractBuilder{{/extendsOtherShape}}{{^extendsOtherShape}}{{TYPES.Client.Util.ObjectBuilderBase}}{{/extendsOtherShape}}{{#needsContainerBuilder}}{{#isOptionalExternallyDiscriminated}} implements {{TYPES.Client.Util.ObjectBuilder}}<{{selfType}}>{{/isOptionalExternallyDiscriminated}}{{/needsContainerBuilder}}{{^needsContainerBuilder}} implements {{TYPES.Client.Util.ObjectBuilder}}<{{selfType}}>{{/needsContainerBuilder}} { private Kind _kind; private {{variantBaseType}} _value; + {{#nonExhaustive}} + private String _customKind; + {{/nonExhaustive}} {{>ObjectShape/Builder/Fields}} public Builder() {} @@ -146,6 +190,9 @@ {{>ObjectShape/Builder/CopyCtorImpl}} this._kind = o._kind; this._value = o._value; + {{#nonExhaustive}} + this._customKind = o._customKind; + {{/nonExhaustive}} } {{#extendsOtherShape}} @@ -171,6 +218,21 @@ {{/type.hasBuilder}} {{/variants}} + {{#nonExhaustive}} + /** + * Set a custom (plugin-provided) variant. + * + * @param type the variant type name as returned by the server + * @param data the raw JSON body of the variant result + */ + public {{^needsContainerBuilder}}{{TYPES.Client.Util.ObjectBuilder}}<{{selfType}}>{{/needsContainerBuilder}}{{#needsContainerBuilder}}ContainerBuilder{{/needsContainerBuilder}} _custom(String type, {{TYPES.Client.Json.JsonData}} data) { + this._kind = Kind._Custom; + this._customKind = {{TYPES.Client.Util.ApiTypeHelper}}.requireNonNull(type, this, ""); + this._value = new CustomVariant({{TYPES.Client.Util.ApiTypeHelper}}.requireNonNull(data, this, "")); + return {{^needsContainerBuilder}}this{{/needsContainerBuilder}}{{#needsContainerBuilder}}new ContainerBuilder(){{/needsContainerBuilder}}; + } + + {{/nonExhaustive}} {{^needsContainerBuilder}}@Override public{{/needsContainerBuilder}}{{#needsContainerBuilder}}{{#isOptionalExternallyDiscriminated}}public{{/isOptionalExternallyDiscriminated}}{{^isOptionalExternallyDiscriminated}}protected{{/isOptionalExternallyDiscriminated}}{{/needsContainerBuilder}} {{selfType}} build() { _checkSingleUse(); @@ -198,4 +260,41 @@ {{>ObjectShape/HashCode}} {{>ObjectShape/Equals}} +{{#nonExhaustive}} + + // Wrapper so JsonData fits the variant interface slot for custom/plugin types + private static final class CustomVariant implements {{variantInterfaceType}}, {{TYPES.Client.Json.PlainJsonSerializable}} { + private final {{TYPES.Client.Json.JsonData}} data; + + CustomVariant({{TYPES.Client.Json.JsonData}} data) { + this.data = data; + } + + public {{TYPES.Client.Json.JsonData}} data() { + return data; + } + + @Override + public Kind _{{#camelCase}}{{className}}{{/camelCase}}Kind() { + return Kind._Custom; + } + + @Override + public void serialize({{TYPES.Jakarta.Json.Stream.JsonGenerator}} generator, {{TYPES.Client.Json.JsonpMapper}} mapper) { + data.serialize(generator, mapper); + } + + @Override + public int hashCode() { + return data.hashCode(); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + return data.equals(((CustomVariant) o).data); + } + } +{{/nonExhaustive}} } diff --git a/java-codegen/src/main/resources/org/opensearch/client/codegen/templates/TaggedUnionShape/Deserialize.mustache b/java-codegen/src/main/resources/org/opensearch/client/codegen/templates/TaggedUnionShape/Deserialize.mustache index 8b398f6800..25f372ea43 100644 --- a/java-codegen/src/main/resources/org/opensearch/client/codegen/templates/TaggedUnionShape/Deserialize.mustache +++ b/java-codegen/src/main/resources/org/opensearch/client/codegen/templates/TaggedUnionShape/Deserialize.mustache @@ -14,6 +14,9 @@ {{#singleKeyMap}} op.setKey(Builder::{{name}}, {{#type}}{{>Type/deserializer}}{{/type}}); {{/singleKeyMap}} + {{#nonExhaustive}} + op.setUnknownFieldHandler((builder, name, parser, mapper) -> builder._custom(name, {{TYPES.Client.Json.JsonData}}._DESERIALIZER.deserialize(parser, mapper))); + {{/nonExhaustive}} } public static final {{TYPES.Client.Json.JsonpDeserializer}}<{{className}}> _DESERIALIZER = {{TYPES.Client.Json.ObjectBuilderDeserializer}}.lazy(Builder::new, {{className}}::setup{{className}}Deserializer, Builder::build); @@ -43,6 +46,12 @@ {{^typeParameters}} public static final {{TYPES.Client.Json.ExternallyTaggedUnion}}.TypedKeysDeserializer<{{className}}> _TYPED_KEYS_DESERIALIZER; + {{#nonExhaustive}} + private static {{className}} _buildCustom(String type, {{TYPES.Client.Json.JsonData}} data) { + return new Builder()._custom(type, data).build(); + } + + {{/nonExhaustive}} static { {{/typeParameters}} {{TYPES.Java.Util.Map}}> deserializers = new {{TYPES.Java.Util.HashMap}}<>(); @@ -51,10 +60,20 @@ {{/variants}} {{#typeParameters}} + {{#nonExhaustive}} + return new {{TYPES.Client.Json.ExternallyTaggedUnion}}.Deserializer<>(deserializers, {{selfType}}::new, (t, d) -> new Builder{{#typeParameters}}{{.}}{{/typeParameters}}()._custom(t, d).build()).typedKeys(); + {{/nonExhaustive}} + {{^nonExhaustive}} return new {{TYPES.Client.Json.ExternallyTaggedUnion}}.Deserializer<>(deserializers, {{selfType}}::new).typedKeys(); + {{/nonExhaustive}} {{/typeParameters}} {{^typeParameters}} + {{#nonExhaustive}} + _TYPED_KEYS_DESERIALIZER = new {{TYPES.Client.Json.ExternallyTaggedUnion}}.Deserializer<>(deserializers, {{selfType}}::new, {{className}}::_buildCustom).typedKeys(); + {{/nonExhaustive}} + {{^nonExhaustive}} _TYPED_KEYS_DESERIALIZER = new {{TYPES.Client.Json.ExternallyTaggedUnion}}.Deserializer<>(deserializers, {{selfType}}::new).typedKeys(); + {{/nonExhaustive}} {{/typeParameters}} } {{/usesTypedKeys}} \ No newline at end of file diff --git a/java-codegen/src/main/resources/org/opensearch/client/codegen/templates/TaggedUnionShape/Serialize.mustache b/java-codegen/src/main/resources/org/opensearch/client/codegen/templates/TaggedUnionShape/Serialize.mustache index cbf1b85eaa..b1db46aff4 100644 --- a/java-codegen/src/main/resources/org/opensearch/client/codegen/templates/TaggedUnionShape/Serialize.mustache +++ b/java-codegen/src/main/resources/org/opensearch/client/codegen/templates/TaggedUnionShape/Serialize.mustache @@ -22,7 +22,12 @@ public void serialize({{TYPES.Jakarta.Json.Stream.JsonGenerator}} generator, {{T if (_kind != null) { {{/isOptionalExternallyDiscriminated}} {{#externallyDiscriminated}} + {{#nonExhaustive}} + generator.writeKey(_kind == Kind._Custom ? _customKind : _kind.jsonValue()); + {{/nonExhaustive}} + {{^nonExhaustive}} generator.writeKey(_kind.jsonValue()); + {{/nonExhaustive}} {{/externallyDiscriminated}} if (_value instanceof {{TYPES.Client.Json.JsonpSerializable}}) { (({{TYPES.Client.Json.JsonpSerializable}}) _value).serialize(generator, mapper);