Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@
import java.util.*;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Stream;

import static com.google.common.base.CaseFormat.LOWER_CAMEL;
import static com.google.common.base.CaseFormat.UPPER_UNDERSCORE;
Expand Down Expand Up @@ -1255,6 +1256,15 @@ public Map<String, ModelsMap> postProcessAllModels(Map<String, ModelsMap> objs)
break;
}
}
// AbstractOpenApiSchema already declares Boolean isNullable(), so a oneOf/anyOf
// member named Nullable gets no is<Member>() check
if (cm.getComposedSchemas() != null) {
Stream.of(cm.getComposedSchemas().getOneOf(), cm.getComposedSchemas().getAnyOf())
.filter(Objects::nonNull)
.flatMap(List::stream)
.filter(member -> "Nullable".equals(sanitizeDataType(member.dataType)))
.forEach(member -> member.vendorExtensions.put("x-is-method-inherited", true));
}
}
}
return objs;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -248,6 +248,21 @@ public class {{classname}} extends AbstractOpenApiSchema{{#vendorExtensions.x-im
setActualInstance(o);
}

{{#composedSchemas}}
{{#anyOf}}
{{^vendorExtensions.x-duplicated-data-type}}
{{^isAnyType}}
{{^isFreeFormObject}}
public {{classname}}({{{dataType}}} o) {
this();
setActualInstance(o);
}

{{/isFreeFormObject}}
{{/isAnyType}}
{{/vendorExtensions.x-duplicated-data-type}}
{{/anyOf}}
{{/composedSchemas}}
static {
{{#composedSchemas}}
{{#anyOf}}
Expand Down Expand Up @@ -328,6 +343,23 @@ public class {{classname}} extends AbstractOpenApiSchema{{#vendorExtensions.x-im
return ({{{dataType}}})super.getActualInstance();
}

{{^vendorExtensions.x-is-method-inherited}}
{{^isContainer}}
{{^isAnyType}}
{{^isFreeFormObject}}
/**
* Check if the actual instance is of the type `{{dataType}}`.
*
* @return true if the actual instance is of the type `{{dataType}}`
*/
public boolean is{{#sanitizeDataType}}{{{dataType}}}{{/sanitizeDataType}}() {
Comment thread
cubic-dev-ai[bot] marked this conversation as resolved.
Resolved
return super.getActualInstance() instanceof {{{dataType}}};
}

{{/isFreeFormObject}}
{{/isAnyType}}
{{/isContainer}}
{{/vendorExtensions.x-is-method-inherited}}
{{/vendorExtensions.x-duplicated-data-type-ignoring-erasure}}
{{/anyOf}}
{{/composedSchemas}}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -298,6 +298,21 @@ public class {{classname}} extends AbstractOpenApiSchema{{#vendorExtensions.x-im
setActualInstance(o);
}

{{#composedSchemas}}
{{#oneOf}}
{{^vendorExtensions.x-duplicated-data-type}}
{{^isAnyType}}
{{^isFreeFormObject}}
public {{classname}}({{{dataType}}} o) {
this();
setActualInstance(o);
}

{{/isFreeFormObject}}
{{/isAnyType}}
{{/vendorExtensions.x-duplicated-data-type}}
{{/oneOf}}
{{/composedSchemas}}
static {
{{#composedSchemas}}
{{#oneOf}}
Expand Down Expand Up @@ -379,6 +394,23 @@ public class {{classname}} extends AbstractOpenApiSchema{{#vendorExtensions.x-im
return ({{{dataType}}})super.getActualInstance();
}

{{^vendorExtensions.x-is-method-inherited}}
{{^isContainer}}
{{^isAnyType}}
{{^isFreeFormObject}}
/**
* Check if the actual instance is of the type `{{dataType}}`.
*
* @return true if the actual instance is of the type `{{dataType}}`
*/
public boolean is{{#sanitizeDataType}}{{{dataType}}}{{/sanitizeDataType}}() {
return super.getActualInstance() instanceof {{{dataType}}};
}

{{/isFreeFormObject}}
{{/isAnyType}}
{{/isContainer}}
{{/vendorExtensions.x-is-method-inherited}}
{{/vendorExtensions.x-duplicated-data-type-ignoring-erasure}}
{{/oneOf}}
{{/composedSchemas}}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4561,6 +4561,62 @@ public void testNativeOneOfAnyOfWithContainerMembers() {
.doesNotContain("getList<String>()", "List<String>.class");
}

@Test
public void testOkHttpGsonOneOfAnyOfTypedConstructors() {
final Path output = newTempFolder();
final CodegenConfigurator configurator = new CodegenConfigurator()
.setGeneratorName(JAVA_GENERATOR)
.setLibrary(JavaClientCodegen.OKHTTP_GSON)
.setInputSpec("src/test/resources/3_0/java/okhttp-gson-composed-typed-constructors.yaml")
.setOutputDir(output.toString().replace("\\", "/"));

List<File> files = new DefaultGenerator().opts(configurator.toClientOptInput()).generate();

validateJavaSourceFiles(files);
final Path modelDir = output.resolve("src/main/java/org/openapitools/client/model");
assertThat(modelDir.resolve("OpenPetStatus.java")).content()
.contains(
"public OpenPetStatus(Object o) {",
"public OpenPetStatus(PetStatus o) {",
"public OpenPetStatus(String o) {",
"public boolean isPetStatus() {",
"return super.getActualInstance() instanceof PetStatus;",
"public boolean isString() {");
assertThat(modelDir.resolve("Scalar.java")).content()
.contains(
"public Scalar(BigDecimal o) {",
"public Scalar(Integer o) {",
"public Scalar(Long o) {",
"public Scalar(Boolean o) {");
// instanceof can't tell List<Pet> from another list, so containers get no is method
assertThat(modelDir.resolve("PetOrPets.java")).content()
.contains("public PetOrPets(Pet o) {", "public PetOrPets(List<Pet> o) {", "public boolean isPet() {")
.doesNotContain("public boolean isList");
// List<BigDecimal> and List<Integer> have the same erasure, so only the first gets a constructor.
assertThat(modelDir.resolve("Numbers.java")).content()
.contains("public Numbers(List<BigDecimal> o) {")
.doesNotContain("public Numbers(List<Integer> o) {");
// Free-form and any-type members are an Object, which the existing constructor already accepts
// and which every instance would match.
assertThat(modelDir.resolve("PetOrAnything.java")).content()
.containsOnlyOnce("public PetOrAnything(Object o) {")
.contains("public PetOrAnything(Pet o) {", "public boolean isPet() {", "public Object getObject() throws ClassCastException {")
.doesNotContain("public boolean isObject() {");
assertThat(modelDir.resolve("PetOrFreeForm.java")).content()
Comment thread
cubic-dev-ai[bot] marked this conversation as resolved.
Resolved
.containsOnlyOnce("public PetOrFreeForm(Object o) {")
.contains("public PetOrFreeForm(Pet o) {", "public boolean isPet() {")
.doesNotContain("public boolean isObject() {");
assertThat(modelDir.resolve("NullablePetOrName.java")).content()
.contains("public NullablePetOrName(Pet o) {", "public NullablePetOrName(String o) {");
// AbstractOpenApiSchema already declares Boolean isNullable()
assertThat(modelDir.resolve("PetOrNullable.java")).content()
.contains("public PetOrNullable(Nullable o) {", "public Nullable getNullable() throws ClassCastException {", "public boolean isPet() {")
.doesNotContain("public boolean isNullable() {");
assertThat(modelDir.resolve("NullableOrName.java")).content()
.contains("public NullableOrName(Nullable o) {", "public Nullable getNullable() throws ClassCastException {", "public boolean isString() {")
.doesNotContain("public boolean isNullable() {");
}

@Test
public void testEnumWithImplements() {
final Path output = newTempFolder();
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
openapi: 3.0.3
info:
title: oneOf/anyOf typed constructors
description: oneOf/anyOf members that need a typed constructor in the okhttp-gson client, and members that must not get one
version: 1.0.0
servers:
- url: http://localhost
paths:
/composed:
get:
operationId: getComposed
responses:
'200':
description: OK
content:
application/json:
schema:
$ref: '#/components/schemas/Composed'
components:
schemas:
Composed:
type: object
properties:
status:
$ref: '#/components/schemas/OpenPetStatus'
scalar:
$ref: '#/components/schemas/Scalar'
petOrPets:
$ref: '#/components/schemas/PetOrPets'
numbers:
$ref: '#/components/schemas/Numbers'
petOrAnything:
$ref: '#/components/schemas/PetOrAnything'
petOrFreeForm:
$ref: '#/components/schemas/PetOrFreeForm'
nullablePetOrName:
$ref: '#/components/schemas/NullablePetOrName'
petOrNullable:
$ref: '#/components/schemas/PetOrNullable'
nullableOrName:
$ref: '#/components/schemas/NullableOrName'
PetStatus:
type: string
enum:
- available
- pending
- sold
OpenPetStatus:
anyOf:
- $ref: '#/components/schemas/PetStatus'
- type: string
Scalar:
oneOf:
- type: number
- type: integer
format: int32
- type: integer
format: int64
- type: boolean
Pet:
type: object
properties:
name:
type: string
PetOrPets:
oneOf:
- $ref: '#/components/schemas/Pet'
- type: array
items:
$ref: '#/components/schemas/Pet'
Numbers:
anyOf:
- type: array
items:
type: number
- type: array
items:
type: integer
PetOrAnything:
anyOf:
- $ref: '#/components/schemas/Pet'
- description: Any value
PetOrFreeForm:
oneOf:
- $ref: '#/components/schemas/Pet'
- type: object
NullablePetOrName:
nullable: true
oneOf:
- $ref: '#/components/schemas/Pet'
- type: string
Nullable:
type: object
properties:
reason:
type: string
PetOrNullable:
oneOf:
- $ref: '#/components/schemas/Pet'
- $ref: '#/components/schemas/Nullable'
NullableOrName:
anyOf:
- $ref: '#/components/schemas/Nullable'
- type: string
Original file line number Diff line number Diff line change
Expand Up @@ -170,6 +170,16 @@ public MyExampleGet200Response(Object o) {
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.

this();
setActualInstance(o);
}

public MyExampleGet200Response(OneOf1 o) {
this();
setActualInstance(o);
}

static {
schemas.put("List<@Valid OneOf1>", List.class);
schemas.put("OneOf1", OneOf1.class);
Expand Down Expand Up @@ -241,6 +251,15 @@ public OneOf1 getOneOf1() throws ClassCastException {
return (OneOf1)super.getActualInstance();
}

/**
* Check if the actual instance is of the type `OneOf1`.
*
* @return true if the actual instance is of the type `OneOf1`
*/
public boolean isOneOf1() {
return super.getActualInstance() instanceof OneOf1;
}

/**
* Validates the JSON Element and throws an exception if issues found
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,11 @@ public MyExamplePostRequest(Object o) {
setActualInstance(o);
}

public MyExamplePostRequest(String o) {
this();
setActualInstance(o);
}

static {
schemas.put("String", String.class);
}
Expand Down Expand Up @@ -179,6 +184,15 @@ public String getString() throws ClassCastException {
return (String)super.getActualInstance();
}

/**
* Check if the actual instance is of the type `String`.
*
* @return true if the actual instance is of the type `String`
*/
public boolean isString() {
return super.getActualInstance() instanceof String;
}

/**
* Validates the JSON Element and throws an exception if issues found
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,16 @@ public SimpleOneOf(Object o) {
setActualInstance(o);
}

public SimpleOneOf(String o) {
this();
setActualInstance(o);
}

public SimpleOneOf(Integer o) {
this();
setActualInstance(o);
}

static {
schemas.put("String", String.class);
schemas.put("Integer", Integer.class);
Expand Down Expand Up @@ -207,6 +217,15 @@ public String getString() throws ClassCastException {
return (String)super.getActualInstance();
}

/**
* Check if the actual instance is of the type `String`.
*
* @return true if the actual instance is of the type `String`
*/
public boolean isString() {
return super.getActualInstance() instanceof String;
}

/**
* Get the actual instance of `Integer`. If the actual instance is not `Integer`,
* the ClassCastException will be thrown.
Expand All @@ -219,6 +238,15 @@ public Integer getInteger() throws ClassCastException {
return (Integer)super.getActualInstance();
}

/**
* Check if the actual instance is of the type `Integer`.
*
* @return true if the actual instance is of the type `Integer`
*/
public boolean isInteger() {
return super.getActualInstance() instanceof Integer;
}

/**
* Validates the JSON Element and throws an exception if issues found
*
Expand Down
Loading
Loading