Skip to content

[java][okhttp-gson] Add typed constructors and is<Member>() checks to oneOf and anyOf models - #25568

Open
SpenserJ wants to merge 4 commits into
OpenAPITools:masterfrom
SpenserJ:java-okhttp-gson-typed-composed-constructors
Open

SpenserJ wants to merge 4 commits into
OpenAPITools:masterfrom
SpenserJ:java-okhttp-gson-typed-composed-constructors

Conversation

@SpenserJ

@SpenserJ SpenserJ commented Nov 5, 2026 •

Copy link
Copy Markdown
Contributor

Summary

okhttp-gson oneOf/anyOf models only have an Object constructor and a getActualInstance() that returns Object, so nothing in the API says which types a union accepts, and reading a value takes an instanceof check against each member. The native library already generates one constructor per member. This adds the same constructors to okhttp-gson, plus an is<Member>() check next to each get<Member>().

For example, a pet status that accepts the known values and any status the server adds later:

PetStatus:
  type: string
  enum: [available, pending, sold]
OpenPetStatus:
  anyOf:
    - $ref: '#/components/schemas/PetStatus'
    - type: string
Pet:
  type: object
  required: [status]
  properties:
    status:
      $ref: '#/components/schemas/OpenPetStatus'
// before
Pet pet = new Pet().status(new OpenPetStatus(PetStatus.AVAILABLE)); // OpenPetStatus(Object)

OpenPetStatus status = pet.getStatus();
if (status.getActualInstance() instanceof PetStatus) {
    PetStatus known = status.getPetStatus();
} else if (status.getActualInstance() instanceof String) {
    String unknown = status.getString();
}

// after
Pet pet = new Pet().status(new OpenPetStatus(PetStatus.AVAILABLE)); // OpenPetStatus(PetStatus)
Pet adopted = new Pet().status(new OpenPetStatus("adopted"));       // OpenPetStatus(String)

OpenPetStatus status = pet.getStatus();
if (status.isPetStatus()) {
    PetStatus known = status.getPetStatus();
} else if (status.isString()) {
    String unknown = status.getString();
}

The generated additions for OpenPetStatus:

 public class OpenPetStatus extends AbstractOpenApiSchema {
     public OpenPetStatus(Object o) { ... }
+    public OpenPetStatus(PetStatus o) { this(); setActualInstance(o); }
+    public OpenPetStatus(String o) { this(); setActualInstance(o); }
     public PetStatus getPetStatus() throws ClassCastException { ... }
+    public boolean isPetStatus() { return super.getActualInstance() instanceof PetStatus; }
     public String getString() throws ClassCastException { ... }
+    public boolean isString() { return super.getActualInstance() instanceof String; }

Members skip what would clash or mislead:

  • Constructors: a member with the same erasure as an earlier one (List<Integer> after List<BigDecimal>) gets none, as in native (x-duplicated-data-type). Free-form and any-type members get none either, because their type is Object and the existing constructor already covers them.
  • is<Member>(): container members get none, because instanceof List can't tell List<Pet> from another list. Free-form and any-type members get none either, because every value is an Object. A member named Nullable gets none, because AbstractOpenApiSchema already declares Boolean isNullable().

The Object constructor stays, so existing code keeps compiling, and a wrong type still fails at runtime in setActualInstance as before.

Evidence

  • Both snippets above compile and run against clients generated from the schema above, before (7.27.0) and after. "available" reads as PetStatus.AVAILABLE, "adopted" reads as the string, and both write back unchanged.
  • New testOkHttpGsonOneOfAnyOfTypedConstructors with spec 3_0/java/okhttp-gson-composed-typed-constructors.yaml, covering enum and string, numeric and boolean, model and list, same-erasure lists, free-form, any-type and nullable members, and a member model named Nullable. JavaClientCodegenTest: 321 passed.
  • A client generated from that spec resolves overloads and checks to the right member: new Scalar(5) is isInteger(), new Scalar(5L) is isLong(), and new PetOrPets(List.of(pet)).isPet() is false.
  • ./bin/generate-samples.sh ./bin/configs/*.yaml changes 32 files in 5 okhttp-gson samples, all of them additions. All 5 samples pass mvn test-compile.

Merge Danger

Door: two-way. Templates only.

Blast Radius: okhttp-gson unions.

Additive, with one source-compatibility edge: when a model has two or more typed constructors, new X(null) with an untyped null no longer compiles because the call is ambiguous. new X() or a cast such as (String) null works.

PR checklist

  • Read the contribution guidelines.
  • Run the following to build the project and update samples:
    ./mvnw clean package || exit
    ./bin/generate-samples.sh ./bin/configs/*.yaml || exit
    ./bin/utils/export_docs_generators.sh || exit
    
    Commit all changed files.
  • If your PR is targeting a particular programming language, @mention the technical committee members, so they are more likely to review the pull request.

@bbdouglas @sreeshas @jfiala @lukoyanov @cbornet @jeff9finger @karismann @Zomzog @lwlee2608 @martin-mfg @KannaKim: review requested for the Java okhttp-gson template change.

@cubic-dev-ai cubic-dev-ai Bot 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.

1 issue found across 36 files

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="samples/client/others/java/okhttp-gson-oneOf-array/src/main/java/org/openapitools/client/model/MyExampleGet200Response.java">

<violation number="1" location="samples/client/others/java/okhttp-gson-oneOf-array/src/main/java/org/openapitools/client/model/MyExampleGet200Response.java:173">
P2: This typed array constructor rejects an otherwise valid empty array. Update `setActualInstance` to accept empty lists (while validating elements for non-empty lists), so `Collections.emptyList()` and JSON `[]` work consistently with the schema.</violation>
</file>

Reply with feedback, questions, or to request a fix.

View guided diff | Re-trigger cubic

setActualInstance(o);
}

public MyExampleGet200Response(List<@Valid OneOf1> o) {

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.

P2: This typed array constructor rejects an otherwise valid empty array. Update setActualInstance to accept empty lists (while validating elements for non-empty lists), so Collections.emptyList() and JSON [] work consistently with the schema.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At samples/client/others/java/okhttp-gson-oneOf-array/src/main/java/org/openapitools/client/model/MyExampleGet200Response.java, line 173:

<comment>This typed array constructor rejects an otherwise valid empty array. Update `setActualInstance` to accept empty lists (while validating elements for non-empty lists), so `Collections.emptyList()` and JSON `[]` work consistently with the schema.</comment>

<file context>
@@ -170,6 +170,16 @@ public MyExampleGet200Response(Object o) {
         setActualInstance(o);
     }
 
+    public MyExampleGet200Response(List<@Valid OneOf1> o) {
+        this();
+        setActualInstance(o);
</file context>

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Not changing this here. The rejection comes from setActualInstance, which has refused empty lists since before this PR. The new constructor just calls it, so new MyExampleGet200Response((Object) List.of()) fails the same way on master. Accepting [] also needs a rule for unions with two list members, which both match it. That changes every okhttp-gson union, so it's better as a separate PR.

@Sekator778 Sekator778 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.

Built it, ran JavaClientCodegenTest and regenerated the okhttp-gson samples; all match and compile. A member model named Nullable compiled on master and fails here on isNullable(). Could the check skip that name, as the constructors skip clashes?

@SpenserJ

SpenserJ commented Nov 9, 2026

Copy link
Copy Markdown
Contributor Author

@Sekator778 thanks for building and checking the samples. Fixed in the latest push: a member named Nullable now gets no is<Member>() check, the same way the constructors skip clashes. It's Nullable only, because isNullable() is the only is*() method AbstractOpenApiSchema declares.

This branch has not been deployed

No deployments
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