Skip to content

Feat/add support for custom kinds - #2072

Open
acote-coveo wants to merge 6 commits into
opensearch-project:mainfrom
acote-coveo:feat/add-support-for-custom-kinds
Open

Feat/add support for custom kinds#2072
acote-coveo wants to merge 6 commits into
opensearch-project:mainfrom
acote-coveo:feat/add-support-for-custom-kinds

Conversation

@acote-coveo

@acote-coveo acote-coveo commented Jul 29, 2026

Copy link
Copy Markdown

Description

This PR adds a _Custom variant kind to all discriminated tagged unions (e.g. Aggregate, Suggest, Query, Property, Processor, Analyzer, TokenFilterDefinition) so that plugin-provided types are captured as raw JSON instead of causing deserialization failures.

When the server returns a discriminator value that isn't natively modeled, the client stores it as Kind._Custom with the original type name accessible via _customKind() and the raw JSON body via _custom(). This enables users to work with plugin-defined aggregations, queries, processors, etc. without waiting for a client release that models the new type.

How It Works

Reading a custom variant:

for (Map.Entry<String, Aggregate> entry : response.aggregations().entrySet()) {
    Aggregate agg = entry.getValue();
    if (agg._isCustom()) {
        String typeName = agg._customKind();   // e.g. "my_plugin_agg"
        JsonData rawData = agg._custom();      // raw JSON body
    }
}

Building a custom variant:

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 discriminated tagged union type.

Changes

Infrastructure (java-client/src/main/java):

  • JsonData._DESERIALIZER: Fix three-arg deserialize overload to correctly handle a pre-consumed parser event.
  • TaggedUnion: Add default _customKind() method returning null for built-in kinds.
  • ExternallyTaggedUnion: Use _customKind() fallback in typed-keys serialization when jsonValue() is null; accept unknownVariantCtor for graceful deserialization of unknown types.

Code generation templates (java-codegen/):

  • TaggedUnionShape.mustache: Add Kind._Custom(null), _customKind field, accessors, CustomVariant inner class with equals/hashCode, null-validated builder method, and updated equals/hashCode on the union itself.
  • Deserialize.mustache: Wire unknownFieldHandler (internally-tagged) and unknownVariantCtor (externally-tagged).
  • Serialize.mustache: Use _customKind when Kind._Custom for the JSON key.

Documentation: Added "Custom (Plugin-Provided) Variant Types" section to guides/json.md.

Tests: Added CustomVariantTest covering deserialization, typed-keys round-trip serialization, internally-tagged round-trip, builder API, and equality semantics.

Related

This approach aligns with the Elasticsearch Java client's OpenTaggedUnion pattern, adapted for OpenSearch's typed variant interfaces.

On ElasticSearch, they specify whether an Tagged Union should be open or not. I took the approach to extend everything that is discriminated to offer as much flexibility as allowed by the plugins.

Issues Resolved

Closes:

… and support custom variant serialization

- Override three-arg deserialize in JsonData._DESERIALIZER to correctly
  use parser.getValue() when the event is already consumed.
- Add default _customKind() method to TaggedUnion interface returning
  null for built-in kinds.
- Fall back to _customKind() in ExternallyTaggedUnion typed-keys
  serialization when jsonValue() is null.
…nions

Add Kind._Custom to every discriminated tagged union to gracefully
handle plugin-provided types. Unknown discriminator values are captured
as _Custom with the raw JSON preserved as JsonData.

- Add _Custom(null) enum constant, _customKind field, _isCustom(),
  _customKind(), _custom() accessors, and Builder._custom(type, data).
- Add CustomVariant inner class with equals/hashCode.
- Use _customKind in serialize when Kind is _Custom.
- Include _customKind in equals()/hashCode().
- Validate type and data with ApiTypeHelper.requireNonNull().
- Wire unknownFieldHandler in internally-tagged deserializers and
  unknownVariantCtor in externally-tagged deserializers.
- Add user guide section with read/build examples.
…and serialization

- Test externally-tagged (Aggregate) deserialization of unknown type.
- Test typed-keys round-trip serialization preserves custom type name.
- Test internally-tagged (Query) round-trip.
- Test Builder._custom() API and equality semantics.
- Update CHANGELOG with feature and fix entries.
WeightedAvg("weighted_avg");
WeightedAvg("weighted_avg"),
/** A custom variant type not natively supported by this client. */
_Custom(null);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the pull request @acote-coveo , mostly all code for the Java client is being generated (this is why every source file has @Generated("org.opensearch.client.codegen.CodeGenerator") annotation and the comment on top).

Please submit the change to the OpenSearch OpenAPI specification so it could be picked up automatically: https://github.com/opensearch-project/opensearch-api-specification/blob/main/spec/schemas/_common.aggregations.yaml#L9

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@reta Thanks for the quick feedback. I just wanted to mention that I didn't explicitly modify the generated, I instead customized the generator template because I assumed that this kind of customization should not be part of the API specification. To me, the API specification should describe what OpenSearch supports. The custom kind that I added is a client-side concern to allow customization over the typed DSL.

For example: https://github.com/opensearch-project/opensearch-java/pull/2072/changes#diff-93745660477f5c12415912b6c105074b530c83c722b2771390de7f4e9b458474R6-R8

Let me know with my answer if you still think this should be document at the API level.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If I were to add this to the API Specification, I would probably add a new custom annotation to mark the variants that are non-exhaustive and from the generator, I would look for this tag to know if I should generate the _custom variant code I just added. The main benefits would be that:

  1. We could control which variants are open or not.
  2. Other generators would have the same signals to generate exhaustive vs non-exhaustive variants.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants