Using correct tag names in Model Resolver of swagger-core when using JAXB annotated Java POJOS
Hello Dear Swagger OS Tools Community,
We utilize a build process, where we generate JAXB-Annotated POJOs from XSD Schema files, then generate / serve OpenAPI files through Swagger / SpringDoc.
This is not the most optimal approach, but since we work with ISO 20022 messages in the banking sector that still works with XSD files rather than JSON Schema and friends, we have to stick with it.
Now I encountered on problem with the Model Resolver in the swagger-core for which I need your help / opinion. To illustrate this, lets consider our JAXB POJOS to look like this:
package io.swagger.v3.core.oas.models.xmltest;
import javax.xml.bind.annotation.XmlElement;
import javax.xml.bind.annotation.XmlRootElement;
@XmlRootElement(name = "Relocation")
public class Relocation {
@XmlElement(name = "Name")
public String name;
@XmlElement(name = "OldAddress")
public Address oldAddress;
@XmlElement(name = "NewAddress")
public Address newAddress;
}
And:
package io.swagger.v3.core.oas.models.xmltest;
import javax.xml.bind.annotation.XmlElement;
import javax.xml.bind.annotation.XmlType;
@XmlType(name = "RelocationAddress", propOrder = {
"Street",
"City"
})
public class ReolcationAddress {
@XmlElement(name = "Street")
public String street;
@XmlElement(name = "City")
public String city;
}
During model resolution, the Model resolver will automatically detect the relevant annotation and build an internal Schema object out of it:
{Relocation=class Schema {
type: object
format: null
$ref: null
description: null
title: null
multipleOf: null
maximum: null
exclusiveMaximum: null
minimum: null
exclusiveMinimum: null
maxLength: null
minLength: null
pattern: null
maxItems: null
minItems: null
uniqueItems: null
maxProperties: null
minProperties: null
required: null
not: null
properties: {name=class StringSchema {
class Schema {
type: string
format: null
$ref: null
description: null
title: null
multipleOf: null
maximum: null
exclusiveMaximum: null
minimum: null
exclusiveMinimum: null
maxLength: null
minLength: null
pattern: null
maxItems: null
minItems: null
uniqueItems: null
maxProperties: null
minProperties: null
required: null
not: null
properties: null
additionalProperties: null
nullable: null
readOnly: null
writeOnly: null
example: null
externalDocs: null
deprecated: null
discriminator: null
xml: class XML {
name: Name
namespace: null
prefix: null
attribute: null
wrapped: null
}
}
}, oldAddress=class Schema {
type: null
format: null
$ref: #/components/schemas/RelocationAddress
description: null
title: null
multipleOf: null
maximum: null
exclusiveMaximum: null
minimum: null
exclusiveMinimum: null
maxLength: null
minLength: null
pattern: null
maxItems: null
minItems: null
uniqueItems: null
maxProperties: null
minProperties: null
required: null
not: null
properties: null
additionalProperties: null
nullable: null
readOnly: null
writeOnly: null
example: null
externalDocs: null
deprecated: null
discriminator: null
xml: class XML {
name: OldAddress
namespace: null
prefix: null
attribute: null
wrapped: null
}
}, newAddress=class Schema {
type: null
format: null
$ref: #/components/schemas/RelocationAddress
description: null
title: null
multipleOf: null
maximum: null
exclusiveMaximum: null
minimum: null
exclusiveMinimum: null
maxLength: null
minLength: null
pattern: null
maxItems: null
minItems: null
uniqueItems: null
maxProperties: null
minProperties: null
required: null
not: null
properties: null
additionalProperties: null
nullable: null
readOnly: null
writeOnly: null
example: null
externalDocs: null
deprecated: null
discriminator: null
xml: class XML {
name: NewAddress
namespace: null
prefix: null
attribute: null
wrapped: null
}
}}
additionalProperties: null
nullable: null
readOnly: null
writeOnly: null
example: null
externalDocs: null
deprecated: null
discriminator: null
xml: class XML {
name: Relocation
namespace: https://www.openapis.org/test/nested
prefix: null
attribute: null
wrapped: null
}
}, RelocationAddress=class Schema {
type: object
format: null
$ref: null
description: null
title: null
multipleOf: null
maximum: null
exclusiveMaximum: null
minimum: null
exclusiveMinimum: null
maxLength: null
minLength: null
pattern: null
maxItems: null
minItems: null
uniqueItems: null
maxProperties: null
minProperties: null
required: null
not: null
properties: {street=class StringSchema {
class Schema {
type: string
format: null
$ref: null
description: null
title: null
multipleOf: null
maximum: null
exclusiveMaximum: null
minimum: null
exclusiveMinimum: null
maxLength: null
minLength: null
pattern: null
maxItems: null
minItems: null
uniqueItems: null
maxProperties: null
minProperties: null
required: null
not: null
properties: null
additionalProperties: null
nullable: null
readOnly: null
writeOnly: null
example: null
externalDocs: null
deprecated: null
discriminator: null
xml: class XML {
name: Street
namespace: null
prefix: null
attribute: null
wrapped: null
}
}
}, city=class StringSchema {
class Schema {
type: string
format: null
$ref: null
description: null
title: null
multipleOf: null
maximum: null
exclusiveMaximum: null
minimum: null
exclusiveMinimum: null
maxLength: null
minLength: null
pattern: null
maxItems: null
minItems: null
uniqueItems: null
maxProperties: null
minProperties: null
required: null
not: null
properties: null
additionalProperties: null
nullable: null
readOnly: null
writeOnly: null
example: null
externalDocs: null
deprecated: null
discriminator: null
xml: class XML {
name: City
namespace: null
prefix: null
attribute: null
wrapped: null
}
}
}}
additionalProperties: null
nullable: null
readOnly: null
writeOnly: null
example: null
externalDocs: null
deprecated: null
discriminator: null
xml: null
}
As you can see, the model successfully extracts the necessary XML Object based on the XmlElement annotation and puts it into the properties list within the Relocation model. However, this information gets lost as soon as it generates an actual OAS Schema out of it:
Relocation:
type: object
properties:
name:
type: string
oldAddress:
$ref: '#/components/schemas/RelocationAddress'
newAddress:
$ref: '#/components/schemas/RelocationAddress'
xml:
name: Relocation
namespace: https://www.openapis.org/test/nested
RelocationAddress:
type: object
properties:
street:
type: string
xml:
name: Street
city:
type: string
xml:
name: City
So reading that schema would mean that the object of type 'Relocation' will have all upper case first letter except 'oldAddress' and 'newAddress', because the XML Object is missing there. This is, however, in-sync with the OAS specification, as it would forbid the use of an XML Object when '$ref' is used at the same time.
Just moving the XML Object from property to type definition would not solve it, as I use 'RelocationAddress' on two occasions with different Tag Names (OldAddress and NewAddress). This example might look trivial at first, but imagine you have something like 'CdtrAcc' and 'DbtrAcc' as XML tags and the actual Java properties are called 'creditorAccount' and 'debtorAccount'.
Now, my bold head came up with two potential approaches:
1) Duplicate the referenced type every time a conflict happens:
Relocation:
type: object
properties:
name:
type: string
oldAddress:
$ref: '#/components/schemas/OldAddress_RelocationAddress'
newAddress:
$ref: '#/components/schemas/NewAddress_RelocationAddress'
xml:
name: Relocation
namespace: https://www.openapis.org/test/nested
OldAddress_RelocationAddress:
type: object
xml:
name: OldAddress
properties:
street:
type: string
xml:
name: Street
city:
type: string
xml:
name: City
NewAddress_RelocationAddress:
type: object
xml:
name: NewAddress
properties:
street:
type: string
xml:
name: Street
city:
type: string
xml:
name: City
This would give the freedom to have different property names for specific tag names, but has also the disadvantage to potentially generate larger OAS files with unnecessary duplication.
2) Always use the Tag name as property name on collision:
Relocation:
type: object
properties:
name:
type: string
OldAddress:
$ref: '#/components/schemas/RelocationAddress'
NewAddress:
$ref: '#/components/schemas/RelocationAddress'
xml:
name: Relocation
namespace: https://www.openapis.org/test/nested
RelocationAddress:
type: object
properties:
street:
type: string
xml:
name: Street
city:
type: string
xml:
name: City
We could tell the model resolver to just overwrite the property definition with its XML tag definition from the XmlElement annotation. The resulting model would be inconsequential for the API consumer, and the model generated from that OAS file would look like "normal code'. The only drawback is that the original 'property" information gets lost during this process. On the other hand, that information might not be that useful anyway from an API design standpoint.
I would personally love to see option 2) implemented, or at least somehow an option to control this generation. Please recall that since we generate JAXB POJOs from an XSD file, we don't have that much of control on the Java property names generated from the XJC tool.
I welcome any alternative suggestion.