Skip to content

[integrations][java] List Jackson enum values in OpenAI, Azure and Anthropic native schemas - #1247

Merged
wenjin272 merged 2 commits into
apache:mainfrom
weiqingy:1246-sdk-enum-schema
Oct 14, 2026
Merged

wenjin272 merged 2 commits into
apache:mainfrom
weiqingy:1246-sdk-enum-schema

Conversation

@weiqingy

Copy link
Copy Markdown
Collaborator

Linked issue: #1246

Purpose of change

When an output schema has an enum whose constants carry @JsonProperty or a @JsonValue method, the native schema sent by the OpenAI Chat Completions, Azure OpenAI and Anthropic connections (and vLLM, which extends the OpenAI one) now lists the values Jackson reads. Before, it listed the Java constant names, the provider held the model to them, and the reply failed to parse.

These three connections built the schema through their SDKs' structured-output builders, which ignore Jackson's enum annotations. Ollama, Gemini, Bedrock and watsonx already use the shared PojoJsonSchemaGenerator, so this moves the three onto it. One enum rule then holds for every Java provider, and it matches Python.

Runtime flow

  1. buildRequest decides whether the schema travels natively, unchanged.
  2. If it does, OpenAIChatCompletionsUtils.toNativeResponseFormat (OpenAI, Azure, vLLM) or AnthropicChatModelConnection.toNativeOutputConfig calls PojoJsonSchemaGenerator.generateAllRequired(schemaClass, FORBIDDEN_ADDITIONAL_PROPERTIES_BY_DEFAULT).
  3. The schema is wrapped into the SDK request object: OpenAI's json_schema response format named json-schema-from-<SimpleName> with strict: true, or Anthropic's output_config.format.

Key decisions

  • A new generateAllRequired next to generate. OpenAI strict mode rejects a schema whose required omits a property, while generate leaves Optional fields out. The new method differs only in the required set; an Optional field stays nullable.
  • Anthropic also requires every field, as its SDK schema did, so the set of fields a reply may omit does not change.
  • The SDK builders are dropped, not tuned. They take no generator options, and the latest openai-java and anthropic-java still use the same bare JacksonModule().
  • One OpenAI helper in OpenAIChatCompletionsUtils replaces the identical copies in the two OpenAI-SDK connections.

Behavioral Semantics

Interaction decisions

Enum constants Listed in the schema, all three connections
No annotations Constant names, unchanged
@JsonProperty on every constant The property values (was: names)
@JsonValue method returning a String The method's values (was: names)
@JsonProperty on only some constants Constant names, unchanged, as on the other four providers
@JsonValue method returning a number The numbers under "type": "string" (was: names), as on the other four providers

Behavioral contracts

  1. An enum annotated on every constant with @JsonProperty, or through a String @JsonValue method, is listed by the values Jackson deserializes.
  2. Every object's properties are all in its required set, at every level; an Optional field is required and admits null.
  3. Every object forbids additional properties.
  4. The OpenAI response format is strict and named after the schema class.
  5. A polymorphic member is still sent as a discriminated union.
  6. When the schema does not travel natively, no response format or output config is added, unchanged.

Failure behavior

  • A reply that does not match the POJO still fails in structured-output parsing, as before. For the two fixed enum shapes it no longer happens when the model follows the schema.
  • A schema the provider rejects surfaces as the provider's error, as before. A numeric @JsonValue enum's schema may now be rejected by the provider where it previously failed at parse time.
  • The SDK path's IllegalStateException for a missing response format or output config is gone; the generator always returns a schema.

Tests

Contract Tests
1 derivedSchemaFollowsJacksonEnumValues in OpenAICompletionsConnectionTest, AzureOpenAIChatModelConnectionTest, AnthropicChatModelConnectionTest
2, 3 derivedSchemaRequiresEveryFieldAndForbidsExtras (OpenAI Completions, Anthropic); PojoJsonSchemaGeneratorTest.allRequiredRequiresOptionalFieldsAsNullable, allRequiredChangesOnlyTheRequiredSet
4 testNativeAppliedForPojoCapableModel, testNativeAppliedForCapableDeploymentModel
5 testPolymorphicMemberSchemaIsSent (all three)
6 Existing testNativeNotApplied*, testNativeSchemaNotAppliedWithoutSchema, testNonClassSchemaKeepsFallback

The three enum tests fail without the connection change, at the enum round-trip, with Jackson rejecting "IN_PROGRESS".

Commands, JDK 17:

  • mvn -B --no-transfer-progress -pl integrations/chat-models/common,integrations/chat-models/openai,integrations/chat-models/anthropic clean test: passed (8, 148, 103 tests, 0 failures).
  • ./tools/check-license.sh: passed.

Not verified:

  • No live call to OpenAI, Azure OpenAI, Anthropic or vLLM.
  • How each provider handles the numeric @JsonValue enum schema.
Schema snapshot before and after, and implementation invariants

Fourteen fixtures were rendered through both the old SDK path and the new one: enums (all @JsonProperty, String @JsonValue, partial @JsonProperty, numeric @JsonValue, plain), Optional with a nested object and @JsonPropertyDescription, collections, primitives, @JsonProperty rename with @JsonIgnore, a getter bean, a @JsonTypeInfo/@JsonSubTypes member, a recursive type, an inherited type, and a plain POJO.

The only differences:

  • Enum arrays for the all-@JsonProperty, String @JsonValue and numeric @JsonValue fixtures.
  • Property order, alphabetical before and declaration order after, in 11 fixtures.

Identical before and after: $schema, every required set, additionalProperties: false at every level, Optional as ["string", "null"], the polymorphic anyOf with const discriminators, descriptions, and the OpenAI format name, strict and type. Before and after, Azure and OpenAI Completions send the same response format, and Anthropic sends the same schema as OpenAI.

Implementation invariants:

  • generateAllRequired and generate share one body and differ only in the required-field check; allRequiredChangesOnlyTheRequiredSet compares the two outputs with required removed.
  • The schema map is built from the ObjectNode's fields in order, so the SDK serializes properties in the generator's order.
  • openai and anthropic now depend on flink-agents-integrations-chat-models-common and jsonschema-generator (BOM-managed), which dist already bundles for the other providers.

API

No public API changes for agent code. PojoJsonSchemaGenerator gains generateAllRequired(Class<?>, Option...); generate is unchanged.

For existing callers of these three connections:

  • Enums annotated as in contract 1 now parse instead of failing.
  • Properties are sent in declaration order instead of alphabetical order, as on the other providers.
  • Swagger 2 @Schema annotations on the schema class are no longer read. Jackson annotations, including @JsonPropertyDescription, still are.
  • Everything else in the schema, and requests without a native schema, are unchanged.

Documentation

  • doc-needed
  • doc-not-needed
  • doc-included

Was this patch authored or co-authored using generative AI tooling?

  • Yes
  • No

Generated-by: GitHub Copilot CLI 1.0.95 (claude-opus-5.5)

…ry field

Adds PojoJsonSchemaGenerator.generateAllRequired, which derives the same
schema as generate except that every field, including an Optional one,
is listed as required. It is the required set OpenAI's strict mode
expects, and it lets the OpenAI, Azure OpenAI and Anthropic connections
move off the SDK's schema builder without changing their required set.

Generated-by: GitHub Copilot CLI 1.0.95 (claude-opus-5.5)
…ed generator

The OpenAI Chat Completions, Azure OpenAI and Anthropic connections derived
their native structured-output schema through the SDK's typed builders. Those
schemas list an enum constant by its Java name even when @JsonProperty or
@jsonvalue names it differently, so strict mode confines the model to values
the response then fails to deserialize from (apache#1246).

Derive the schema with PojoJsonSchemaGenerator.generateAllRequired instead,
which lists enum constants the way Jackson deserializes them and keeps the
strict-mode shape the SDK schemas had: every property required (an Optional
field admits null) and no additional properties. The two OpenAI copies of the
helper become one in OpenAIChatCompletionsUtils; vLLM inherits the fix through
OpenAICompletionsConnection.

The schema otherwise matches the SDK's, except that properties now follow
declaration order instead of alphabetical order, and Swagger 2 @Schema
annotations are no longer read.

Generated-by: GitHub Copilot CLI 1.0.95 (claude-opus-5.5)
@github-actions github-actions Bot added doc-not-needed Your PR changes do not impact docs fixVersion/0.4.0 priority/major Default priority of the PR or issue. labels Oct 13, 2026

@wenjin272 wenjin272 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Thanks for fixing this. LGTM

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

Labels

doc-not-needed Your PR changes do not impact docs fixVersion/0.4.0 priority/major Default priority of the PR or issue.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants