OpenAPITools / OpenAPITools/openapi-generator

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

Open
#11,347 1 comment 1 reaction 0 assignees View on GitHub

Nobody has claimed this yet.

Issue: Bug
Dominant language
Java
Stars
26.8k
Forks
7.7k
PR merge metrics
PR metrics pending

Description

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.

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start with the generation configuration in build.gradle and test-specification.yml, then inspect the generated ProductOne and ProductTwo classes. Run TestOrderSpec and compare the serialized round-trip output with the input JSON; done means the discriminator field appears only once without hand-editing generated code.

Written by the indexing model from the issue text.

Assessment

Tech stack
java, kotlin
Domain
api, tooling
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Stale
Clarity
Mostly clear
Newbie friendliness
38/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.