OpenAPITools / OpenAPITools/openapi-generator
[BUG][KOTLIN][JACKSON] JSON serialization of HTTP body data types yields duplicate ouput of field used as polymorphism discriminator
Nobody has claimed this yet.
- 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
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- 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