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

SP
1d ago
+1109-0

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.