5

I'm using OpenAPI 3.0 inheritance in components schemas and I have the (Java) classes generated by openapi-generator (which uses Jackson).

Why the discriminator property gets serialized twice in the resulting JSON?

This is a JHipster API-First project, which should use openapi-generator for generating the Java model (POJOs with Jackson annotations) and API controllers (interfaces with Spring's @Api annotations).

By following the OpenAPI 3.x documentation/examples, it seems that the property used as discriminator must also be specified in the properties list of the schema.

This way, the generated Java class seems to differ from the Jackson guidelines for polymorphic type handling with annotations (here), where the property used as discriminator must not be present in the class. Instead, the generated code also includes this property as a class attribute with getter/setter. This causes the JSON output to include the property twice, as shown below.

I've also tried to remove the property from the OpenAPI properties list, leaving intact the discriminator part; this way the generated code corresponds to the Jackson's guidelines and the serialization works just fine. On the other hand, I get an error during the deserialization process because the (removed) property is not found in the target class.

Following the OpenAPI 3.x doc guidelines:

TicketEvent:
  type: object
  description: A generic event
  discriminator:
    propertyName: type
  required:
    - id
    - sequenceNumber
    - timestamp
    - type
  properties:
    id:
      type: integer
      format: int64
      readOnly: true
    sequenceNumber:
      type: integer
      readOnly: true
    timestamp:
      type: string
      format: date-time
      readOnly: true
    type:
      type: string
      readOnly: true
TicketMovedEvent:
  description: A ticket move event
  allOf:
    - $ref: '#/components/schemas/Event'
    - type: object
      required:
        - source
        - target
      properties:
        source:
          $ref: '#/components/schemas/Queue'
        target:
          $ref: '#/components/schemas/Queue'

Generated class:

@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.PROPERTY, property = "type", visible = true)
@JsonSubTypes({
  @JsonSubTypes.Type(value = TicketMovedEvent.class, name = "TicketMovedEvent")
})

public class TicketEvent   {
   ...

   @JsonProperty("type")
   private String type;

The JSON includes the property twice:

{
        ...
    "type": "TicketMovedEvent",
    "type": null,
        ...
}

Removing the discriminator property from properties list:

TicketEvent:
  type: object
  description: A generic event
  discriminator:
    propertyName: type
  required:
    - id
    - sequenceNumber
    - timestamp
  properties:
    id:
      type: integer
      format: int64
      readOnly: true
    sequenceNumber:
      type: integer
      readOnly: true
    timestamp:
      type: string
      format: date-time
      readOnly: true

Generated class without type property:

@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.PROPERTY, property = "type", visible = true)
@JsonSubTypes({
  @JsonSubTypes.Type(value = TicketMovedEvent.class, name = "TicketMovedEvent")
})

public class TicketEvent   {
   ...

   // now the "type" property is missing
})

The JSON now is correct:

{
        ...
    "type": "TicketMovedEvent",
        ...
}

I would expect that, by following the OpenAPI 3.x guidelines, the generated class to be properly serialized/deserialized.

(sidenote)

During deserialization, by using the aforementioned approach, you might get the following error:

com.fasterxml.jackson.databind.exc.UnrecognizedPropertyException: Unrecognized field "type" (class it.blutec.bludesk.web.api.model.TicketMovedEvent), not marked as ignorable ...

To fix this, you need to configured the Jackson ObjectMapper object to ignore this kind of situations.

ObjectMapper om = new ObjectMapper().configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false)
nickshoe
  • 53
  • 2
  • 9
  • Open an issue in the openapi-generator repository https://github.com/OpenAPITools/openapi-generator/issues – Helen Aug 29 '19 at 09:13
  • 1
    @Helen thanks: https://github.com/OpenAPITools/openapi-generator/issues/3796 – nickshoe Aug 29 '19 at 10:00
  • I came across a similar issue (I was not using OpenAPI though, just plain Jackson and I had full control over the source code). If I remember it correctly, the fix was to use `JsonTypeInfo.As.EXISTING_PROPERTY` instead of `JsonTypeInfo.As.PROPERTY`. – cassiomolin Aug 29 '19 at 10:07
  • @cassiomolin I've tried to manually override the generated class with `JsonTypeInfo.As.EXISTING_PROPERTY`, this way the "discriminator" is only present once in the JSON but it has `null` value. I think that is something related to the generated code for Spring, actually I'm trying to find some clues in the openapi-generator repo. Thank you. – nickshoe Aug 30 '19 at 09:59

5 Answers5

4

I Just Ran into the same issue. The problem is with the @JsonTypeInfo annotation on the generated model class.

Its being generated as -

@JsonTypeInfo(... include = JsonTypeInfo.As.PROPERTY ...)

which adds an extra property in the model.

If I change it in the generated code to -

@JsonTypeInfo(... include = JsonTypeInfo.As.EXISTING_PROPERTY ...)

It uses the property which is already in the model and everything works fine.

However, of-course this change is lost when I regenerate the code. It'd be great if someone has found a permanent solution to this.

Soham
  • 126
  • 1
  • 6
  • Exactly. An year ago I opened an [issue](https://github.com/OpenAPITools/openapi-generator/issues/3796), if you want to track this. – nickshoe Sep 01 '20 at 09:12
  • I see that someone has merged a PR - https://github.com/OpenAPITools/openapi-generator/pull/5156/files which changes the type info annotation to EXISTING_PROPERTY. – Soham Sep 02 '20 at 15:34
  • Yes, but that seemed not to be working for the purpose of this issue. Please, check my [comment](https://github.com/OpenAPITools/openapi-generator/issues/3796#issuecomment-627874116). Anyways, I'll give it a try again. – nickshoe Sep 03 '20 at 13:34
  • (I was supporting the `EXISTING_PROPERTY` solution in my comment above because I forgot it had not work for me) – nickshoe Sep 03 '20 at 13:53
3

Just want to add a workaround for the issue described in Soham's answer.

If you are using spring-boot-maven-plugin you can override the mustache template in the project to fix the generated code.

Add a folder in the project's root named openapi-generator-templates.

In the pom.xml file add this line in the plugin's configuration node (NOT configOptions)

<templateDirectory>${project.basedir}/openapi-generator-templates</templateDirectory>

Inside the folder copy the mustache template from openapi-generator project on Github.

I am using kotlin-spring generator so for me the template was here.

Edit the mustache file replacing JsonTypeInfo.As.PROPERTY with JsonTypeInfo.As.EXISTING_PROPERTY

(As a side note, in kotlin-spring generator to have inheritance working you should also use a similar workaround for the dataClass.mustache template. More info about this bug here).

Ena
  • 3,481
  • 36
  • 34
  • This is a nice advice, I'll give it a try. A question: should I copy all the templates in the new folder, or can I just override the one that I need by creating onlu the corresponding file? – nickshoe Jul 06 '22 at 20:14
  • 1
    You should just override the ones you need. – Ena Jul 08 '22 at 12:15
  • I also used this solution and can confirm that it works on Kotlin generator. Editing the required mustache file solves the problem like a charm – ozgurc Feb 20 '23 at 08:55
2

Just ran into this myself. Not including the discriminator field in the OpenApi properties list does indeed take care of the double-field problem upon serialization, while causing an UnrecognizedPropertyException upon deserialization. With some trial and error, I found that the 2nd problem can be solved by deleting the "visible = true" property (or setting it to false) of the @JsonTypeInfo annotation in the generated code. (If you have your build process setup to always re-generate code from the open API spec then, of course, this is not a real solution).

AgentP
  • 6,261
  • 2
  • 31
  • 52
  • Yeah, the problem relies in the generation process.. manually editing generated classes can be a workaround, but I wish it will be fixed eventually. If you want to track the issue, here is the GitHub link: https://github.com/OpenAPITools/openapi-generator/issues/3796 – nickshoe Oct 30 '19 at 15:11
1

I believe I solved this in https://github.com/OpenAPITools/openapi-generator/pull/5120 which is supposed to be released with openapi-generator 4.3 release. Feel free to check it out (the oneOf support should also be vastly improved thanks to that PR).

slavek
  • 401
  • 3
  • 4
  • I've tried the 4.3.0-SNAPSHOT (building it on my machine), with "spring" target, but the discriminator property is still there in the generated class. My suggestion is to avoid generating the discriminator property in the class, since this info is already present thanks to the (Jackson) `JsonTypeInfo` annotation. I've opened an issue for this: https://github.com/OpenAPITools/openapi-generator/issues/3796 – nickshoe Feb 07 '20 at 16:32
1

As Florimon says in an earlier answer, you can eliminate the double discriminator field serialization problem by not including the discriminator in the OpenApi properties list, which then causes UnrecognizedPropertyException on deserialization. And that can be fixed by removing "visible=true" property (or setting to false) in the @JsonTypeInfo annotation. Which is painful for generated code.

A workaround is to use Jackson-mix in annotations to replace the generated @JsonTypeInfo annotations. Create a mixin class with the desired visible=false like this:

@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.PROPERTY, visible = false, property = "discriminator")
abstract class MyMixIn {}

Add register with your ObjectMapper like this:

objectMapper.addMixIn(MyGenerated.class, MyMixIn.class)

You should also be able to customize the ObjectMapper used in client/server to have it use the mix in. For example, in a jax-rs server application, you can implement and register a @Provider class which implements ContextResolver<ObjectMapper>.

On the client side, register it with something like this:

ClientBuilder.newClient().register(JacksonFeature.class).register(MyObjectMapperProvider.class)