Skip to content

[BUG][KOTLIN][JACKSON] JSON serialization of HTTP body data types yields duplicate ouput of field used as polymorphism discriminator #11347

@drmoeller

Description

@drmoeller
Description

Currently I'm using OpenAPI generator only to generate Kotlin model types representing exchanged HTTP body entities.

As far as I can tell deserialization of JSON entities from HTTP request bodies works as expected; but I've recognized deviation when serializing received object graphs back to JSON in response bodies:
Duplication of JSON-field used as polymorphism discriminator.

openapi-generator version

5.3.1 (currently resolved from wildcard declaration 5.+ within build.gradle file)

OpenAPI declaration file content or url

I've simplified OpenAPI specification test-specification.yml for easier testing:

openapi: 3.0.3
info:
  title: Test Case
  version: 0.0.1
paths:
  /dummy:
    get:
      responses:
        200:
          description: Dummy.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Order'
components:
  schemas:
    Order:
      type: object
      required:
        - businessID
        - product
      properties:
        businessID:
          type: string
        product:
          $ref: '#/components/schemas/Product'
    ProductTypeEnum:
      type: string
      enum:
        - one
        - two
    Product:
      type: object
      required:
        - type
        - name
      properties:
        type:
          $ref: "#/components/schemas/ProductTypeEnum"
        name:
          type: string
      discriminator:
        propertyName: type
        mapping:
          one: "#/components/schemas/ProductOne"
          two: "#/components/schemas/ProductTwo"
    ProductOne:
      allOf:
        - $ref: "#/components/schemas/Product"
        - type: object
          required:
            - attribute
          properties:
            attribute:
              type: string
    ProductTwo:
      allOf:
        - $ref: "#/components/schemas/Product"
        - type: object
          required:
            - capacity
          properties:
            capacity:
              type: number

OpenAPI specification declares schemata of central entity Order having two fields, one of them polymorphic with two possible sub-types of Product: ProductOne and ProductTwo. Field type is used as discriminator.

Generation Details

Corresponding chapter from Gradle build file build.gradle looks like so:

plugins {
  [...]
  id('org.openapi.generator') version '5.+'
}

[...]

  openApiGenerate {
    generatorName = 'kotlin'
    inputSpec = "${rootDir}/test-specification.yml"
    outputDir = "${buildDir}/openapi-generated-test"
    modelPackage = 'test'
    configOptions = [
      dateLibrary         : 'java8',
      enumPropertyNaming  : 'original',
      serializationLibrary: 'jackson'
    ]
    globalProperties = [
      models   : '',
      modelDocs: 'false'
    ]
  }

[...]

As you can see, jackson is configured as serializationLibrary, cause I'm already using this lib for other JSON-related purposes within the project.

Steps to reproduce
package test

import com.fasterxml.jackson.databind.ObjectMapper
import com.fasterxml.jackson.module.kotlin.KotlinModule
import spock.lang.Specification

import static com.fasterxml.jackson.annotation.JsonInclude.Include.NON_NULL
import static groovy.json.JsonOutput.prettyPrint

class TestOrderSpec extends Specification {

  def JSON_MAPPER = new ObjectMapper()
    .registerModules(new KotlinModule())
    .setSerializationInclusion(NON_NULL)

  def 'JSON deserialization/serialization round trip'() {
    when:
    Order deserializedOrder = JSON_MAPPER.readValue(testOrderJSON, Order)

    then:
    with(deserializedOrder) {
      businessID == 'BID_4711'
      with(product) {
        type == TestProductTypeEnum.two
        name == 'Name of connection'
        capacity == BigDecimal.valueOf(42L)
      }
    }

    when: 'Serialization back to JSON yields same result'
    def backSerialization = JSON_MAPPER.writerFor(Order).writeValueAsString(deserializedOrder)

    then:
    prettyPrint(backSerialization) == prettyPrint(testOrderJSON)

    where:
    testOrderJSON = """{
  "businessID":"BID_4711",
  "product":{
    "type": "two",
    "name":"Name of product",
    "capacity":42.0
  }
}"""
  }

}
Related issues/PRs

Unknown

Suggest a fix

Here is generated Kotlin code of one of the types involved (removed comments, @file:Suppress annotation, etc.):

package test

import test.Product
import test.ProductTwoAllOf
import test.ProductTypeEnum

import com.fasterxml.jackson.annotation.JsonProperty

data class ProductTwo (

    @field:JsonProperty("type")
    override val type: ProductTypeEnum,

    @field:JsonProperty("name")
    override val name: kotlin.String,

    @field:JsonProperty("capacity")
    val capacity: java.math.BigDecimal

) : Product

Provided Spock-driven test case uses following pretty-printed JSON to get deserialized to the generated data types (using Jackson framework):

{
  "businessID":"BID_4711",
  "product":{
    "type": "two",
    "name":"Name of product",
    "capacity":42.0
  }
}

Test case shows proper deserialization to the expected graph of instances, all fields contain the expected content!

But, when serializing the received object graph back to JSON, I got the following result:

{
  "businessID":"BID_4711",
  "product":{
    "type": "two",
    "type": "two",
    "name":"Name of product",
    "capacity":42.0
  }
}

As you can see, discriminator field type appears twice!

Playing around with some options, I found a solution to remove this duplicate line by modifying generated code by hand.
Within classes ProductOne and ProductTwo, add access = JsonProperty.Access.WRITE_ONLY instruction with JsonProperty annotation on type field like so:

  [...]
  @field:JsonProperty("type", access = JsonProperty.Access.WRITE_ONLY)
  override val type: TestProductTypeEnum,
  [...]

I've not deeply tested this modification for side effects, but it reproducibly solves the problem at hand:
Deserialising JSON to an object graph and serializing it back to JSON yields exactly the same pretty-printed JSON document (pretty-printed to remove issues with line breaks, etc.).

So I suggest to modify code generator to generate shown slightly changed annotation to solve this issue.

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions