openapi-generator
openapi-generator copied to clipboard
[BUG][SPRING] oneOf type: Jackson trying to instantiate interface instead of implementation
Description
I'm using the oneOf feature to define several possible schemas that can go into a response body of my service. In the generated Java client code, the Java implementations of these schemas implement an interface, but when I send a request through, Jackson is trying to create an instance of the interface, instead of the concrete class.
The same issue was reported and fixed in swagger-codegen project: https://github.com/swagger-api/swagger-codegen/issues/10011
openapi-generator version
6.4.0
OpenAPI declaration file content or url
openapi: 3.0.1
info:
version: '2.0'
title: ABC API documentation
...
responses:
...
...
CancellationDuration:
type: object
oneOf:
- $ref: '#/components/schemas/DeadlineAbsolute'
- $ref: '#/components/schemas/DeadlineDurationBeforeArrival'
Generation Details
openapi-generator-maven-plugin is used, full configuration:
<plugin>
<groupId>org.openapitools</groupId>
<artifactId>openapi-generator-maven-plugin</artifactId>
<version>6.4.0</version>
<configuration>
<generateSupportingFiles>true</generateSupportingFiles>
<generatorName>spring</generatorName>
<generateApiDocumentation>true</generateApiDocumentation>
<generateModelDocumentation>true</generateModelDocumentation>
<configOptions>
<oas3>true</oas3>
<useTags>true</useTags>
<withSeparateModelsAndApi>true</withSeparateModelsAndApi>
<delegatePattern>true</delegatePattern>
<withInterfaces>true</withInterfaces>
<library>spring-boot</library>
<dateLibrary>java8</dateLibrary>
<useSpringfox>false</useSpringfox>
<useSpringController>true</useSpringController>
<modelPropertyNaming>camelCase</modelPropertyNaming>
</configOptions>
<!-- Option to prevent openapi generator to strip off common prefix from enum values -->
<additionalProperties>removeEnumValuePrefix=false</additionalProperties>
<!-- <skipIfSpecIsUnchanged>true</skipIfSpecIsUnchanged>-->
</configuration>
<executions>
<execution>
<id>spring-execution</id>
<goals>
<goal>generate</goal>
</goals>
<configuration>
<ignoreFileOverride>${project.basedir}/.openapi-generator-ignore</ignoreFileOverride>
<inputSpec>${project.basedir}/src/main/resources/yaml/apis/api/Api.yaml</inputSpec>
<generatorName>spring</generatorName>
<apiPackage>com.mycompany.obt.master.apis</apiPackage>
<modelPackage>com.mycompany.obt.master.models</modelPackage>
</configuration>
</execution>
<execution>
<id>typescript-execution</id>
<goals>
<goal>generate</goal>
</goals>
<configuration>
<inputSpec>${project.basedir}/src/main/resources/yaml/apis/api/Api.yaml</inputSpec>
<generatorName>typescript-axios</generatorName>
<apiPackage>com.mycompany.obt.master.apis</apiPackage>
<modelPackage>com.mycompany.obt.master.models</modelPackage>
</configuration>
</execution>
</executions>
</plugin>
This results in an empty interface
@Generated(value = "org.openapitools.codegen.languages.SpringCodegen", date = "2023-03-31T09:08:52.189665+05:30[Asia/Kolkata]")
public interface CancellationDuration {}
And two concrete classes:
@Schema(name = "DeadlineAbsolute", description = "Absolute Date and time before which cancellation policy is applicable")
@Generated(value = "org.openapitools.codegen.languages.SpringCodegen", date = "2023-03-31T09:13:38.604952+05:30[Asia/Kolkata]")
public class DeadlineAbsolute implements CancellationDuration { ... }
@Schema(name = "DeadlineDurationBeforeArrival", description = "Time before arrival when cancellation policy is applicable")
@Generated(value = "org.openapitools.codegen.languages.SpringCodegen", date = "2023-03-31T09:13:38.604952+05:30[Asia/Kolkata]")
public class DeadlineDurationBeforeArrival implements CancellationDuration { ... }
This error occurs on deserialization:
Caused by: org.springframework.http.converter.HttpMessageConversionException: Type definition error: [simple type, class com.mycompany.obt.master.models.CancellationDuration]; nested exception is com.fasterxml.jackson.databind.exc.InvalidDefinitionException: Cannot construct instance of `com.mycompany.obt.master.models.CancellationDuration` (no Creators, like default constructor, exist): abstract types either need to be mapped to concrete types, have custom deserializer, or contain additional type information
at [Source: (org.springframework.util.StreamUtils$NonClosingInputStream); line: 1762, column: 32]
at org.springframework.http.converter.json.AbstractJackson2HttpMessageConverter.readJavaType(AbstractJackson2HttpMessageConverter.java:388)
at org.springframework.http.converter.json.AbstractJackson2HttpMessageConverter.read(AbstractJackson2HttpMessageConverter.java:343)
at org.springframework.web.servlet.mvc.method.annotation.AbstractMessageConverterMethodArgumentResolver.readWithMessageConverters(AbstractMessageConverterMethodArgumentResolver.java:185)
at org.springframework.web.servlet.mvc.method.annotation.RequestResponseBodyMethodProcessor.readWithMessageConverters(RequestResponseBodyMethodProcessor.java:160)
at org.springframework.web.servlet.mvc.method.annotation.RequestResponseBodyMethodProcessor.resolveArgument(RequestResponseBodyMethodProcessor.java:133)
at org.springframework.web.method.support.HandlerMethodArgumentResolverComposite.resolveArgument(HandlerMethodArgumentResolverComposite.java:122)
at org.springframework.web.method.support.InvocableHandlerMethod.getMethodArgumentValues(InvocableHandlerMethod.java:179)
same issue
same problem here
Same on 6.6.0
In order to fix this issue you need to define the discriminator object for the oneOf relation:
CancellationDuration:
type: object
oneOf:
- $ref: '#/components/schemas/DeadlineAbsolute'
- $ref: '#/components/schemas/DeadlineDurationBeforeArrival'
discriminator:
propertyName: refType
All possible schemas (in this case DeadlineAbsolute and DeadlineDurationBeforeArrival) need a property with the discriminator property name of type string.
After that the generated class (CancellationDuration.class) should contain the class annotations @JsonTypeInfo
and @JsonSubTypes
which enables Jackson to perform the instantiation correctly.
I do not know if ithis way of implementing the oneOf type is expected but it was the only working solution I found.
(Tested with openapi-generator version 6.6.0)
same issue
If is useful in your case (one implementation is an "extension" of the others), you can define implementation which will be used instead of interface. https://stackoverflow.com/questions/12688503/jackson-how-to-specify-a-single-implementation-for-interface-referenced-deseri
In order to fix this issue you need to define the discriminator object for the oneOf relation:
CancellationDuration: type: object oneOf: - $ref: '#/components/schemas/DeadlineAbsolute' - $ref: '#/components/schemas/DeadlineDurationBeforeArrival' discriminator: propertyName: refType
All possible schemas (in this case DeadlineAbsolute and DeadlineDurationBeforeArrival) need a property with the discriminator property name of type string.
After that the generated class (CancellationDuration.class) should contain the class annotations
@JsonTypeInfo
and@JsonSubTypes
which enables Jackson to perform the instantiation correctly.I do not know if ithis way of implementing the oneOf type is expected but it was the only working solution I found.
(Tested with openapi-generator version 6.6.0)
this immediately worked for me! and we dont even need to send the value of refType
- it's just ignored
thank you @Dimibe
This bug is still problematic for objects without discriminators. As far as I can see there is no reason preventing from generating @JsonSubTypes annotation on the interface...
We have the same issue here, would be great if this could be fixed.
Sam Issue and has been persisting for a while now. Clean workaround does not exist.
We are screwed with almost same situation, our problem is that our discriminator is in outer scope of the dynamic object:
{
"itemType": "CommunicationEvent", # this is discriminator
"itemDate": "2019-08-24T14:15:22Z",
"itemId": "DBF4732B-B8EB-44C5-9988-2C07EC765CB2",
"itemDetail": { # this object changes based on discriminator
"communicationEventId": "DBF4732B-B8EB-44C5-9988-2C07EC765CB2",
"communicationEventCreationDate": "2019-08-24T14:15:22Z",
"eventName": "string",
"communicationEventStatusCode": "string",
"validTo": "2019-08-24T14:15:22Z"
}
}
So far, I have not found any better solution than using 3 gson instances:
GsonBuilder gsonBuilder = new GsonBuilder().setPrettyPrinting()
.registerTypeAdapter(OffsetDateTime.class, (JsonDeserializer<OffsetDateTime>)
(json, type, context) -> OffsetDateTime.parse(json.getAsString()));
Gson gson4Com = gsonBuilder.registerTypeAdapter(
ItemDetailForSearch.class,
InterfaceSerializer.interfaceSerializer(CommunicationForSearch.class)
).create();
Gson gson4ComEvent = gsonBuilder.registerTypeAdapter(
ItemDetailForSearch.class,
InterfaceSerializer.interfaceSerializer(CommunicationEventForSearch.class)
).create();
Gson gson4Docu = gsonBuilder.registerTypeAdapter(
ItemDetailForSearch.class,
InterfaceSerializer.interfaceSerializer(DocumentForSearch.class)
).create();
Gson gson = gsonBuilder
.registerTypeAdapter(SearchResult.class, (JsonDeserializer<SearchResult>) (json, typeOfT, context) -> {
ItemType itemType = ItemType.fromValue(json.getAsJsonObject().get("itemType").getAsString());
return switch (itemType) {
case COMMUNICATION -> gson4Com.fromJson(json, SearchResult.class);
case COMMUNICATIONEVENT -> gson4ComEvent.fromJson(json, SearchResult.class);
case DOCUMENT -> gson4Docu.fromJson(json, SearchResult.class);
};
})
.create();
Same issue. Any news?
I have the same issue also.
I tried to use my own jackson deserializer of the parent interface, and tell the generator to put in on the parent interface at generation time by using x-class-extra-annotation, but unfortunately this does not work. I don't know if it's intended or if it's a bug.
openapi contract:
Tax:
x-class-extra-annotation: "@com.fasterxml.jackson.databind.annotation.JsonDeserialize(using = my.package.TaxDeserializer.class)"
description: A common object to describe a tax.
oneOf:
- $ref: "#/components/schemas/TaxWithFixedAmount"
- $ref: "#/components/schemas/TaxWithPercentage"
- $ref: "#/components/schemas/TaxWithCode"
custom deserializer:
public class TaxDeserializer extends StdDeserializer<Tax> {
public TaxDeserializer() {
this(null);
}
public TaxDeserializer(Class<?> vc) {
super(vc);
}
@Override
public Tax deserialize(JsonParser jsonParser, DeserializationContext deserializationContext) throws IOException {
var node = (JsonNode) jsonParser.getCodec().readTree(jsonParser);
...
}
}
generated Tax
interface (without the wanted @JsonDeserialize annotation):
@Generated(value = "org.openapitools.codegen.languages.SpringCodegen")
public interface Tax {
}
I don't want to use a discriminator property because I don't want to break my api contract (already in production), and also because I don't want to force the consumers of my api to populate this field.
With the existing properties of the 3 different implementations of Tax
, I can dynamically compute which one it is, no need for an extra property...
Same issue. Any news?
Run into this kind of issue where my specification for property type to be either number or spring produces empty interface and an error when trying to use the API:
schemas:
MetricBody:
type: object
properties:
key:
type: string
example: visit_counter_total
value:
oneOf:
- type: integer
- type: string
example: 32
@Generated(value = "org.openapitools.codegen.languages.SpringCodegen", date = "2024-06-11T14:58:06.997624700+03:00[Europe/Moscow]", comments = "Generator version: 7.5.0") public interface MetricBodyValue { }