How do you set a default discriminator for each child class?
For example, take this schema:
components:
schemas:
Pet:
type: object
required:
- petType
properties:
petType:
type: string
discriminator:
propertyName: petType
Cat:
allOf:
- $ref: '#/components/schemas/Pet'
- type: object
# all other properties specific to a `Cat`
properties:
name:
type: string
Dog:
allOf:
- $ref: '#/components/schemas/Pet'
- type: object
# all other properties specific to a `Dog`
properties:
bark:
type: string
Code-generators for the above schema will create a client where the petType value must be explicitly set by the programmer. Why can't the Cat object have the petType be set to Cat by default?
I tried using default value to get it to work. However, the resulting code includes shadowed properties (same property on the child and parent).
components:
schemas:
Pet:
type: object
required:
- petType
properties:
petType:
type: string
discriminator:
propertyName: petType
Cat:
allOf:
- $ref: '#/components/schemas/Pet'
- type: object
# all other properties specific to a `Cat`
properties:
name:
type: string
petType:
type: string
default: 'Cat'
Dog:
allOf:
- $ref: '#/components/schemas/Pet'
- type: object
# all other properties specific to a `Dog`
properties:
bark:
type: string
petType:
type: string
default: 'Dog'
Removing the property petType from the parent also doesn't feel right since technically it's more of a property of the parent than the child.
components:
schemas:
Pet:
type: object
required:
- petType
discriminator:
propertyName: petType
Cat:
allOf:
- $ref: '#/components/schemas/Pet'
- type: object
# all other properties specific to a `Cat`
properties:
name:
type: string
petType:
type: string
default: 'Cat'
Dog:
allOf:
- $ref: '#/components/schemas/Pet'
- type: object
# all other properties specific to a `Dog`
properties:
bark:
type: string
petType:
type: string
default: 'Dog'
Do you have any solutions to this problem?
The OAS Specification doesn't natively support a default discriminator. The OAS specification expected every field to contain a value to specify which child model to use (discriminator). At most, all you can hope to do is define an alternate field key to simplify the discriminator (called a mapping).
The openapi-generator follows the OAS specification pretty closely. Because of this, there are no specific settings within the OpenApi Generator to explicitly define a default discriminator for your polymorphic models.
This means that every model will have to have a field defined within it to be used as the discriminator.
Thanks to the customizability of the openapi-generator, you can work around this however you choose. This is obviously highly dependent on which generator you are using, but for the purposes of this demonstration, I will be using the spring generator.
Because the spring generator uses Jackson, we can examine Jackson to see how it can work with polymorphic type. As of Jackson 2.12, Jackson has added a new @JsonTypeInfo called Deduction. This new Type tells Jackson to guess which model to deserialize to based on the values of the serialized object. For example, the below class would work to deserialize an object with name: Tiger into a Cat object, because only the Cat object has a value for name.
@JsonTypeInfo(use = JsonTypeInfo.Id.DEDUCTION)
@JsonSubTypes({
@JsonSubTypes.Type(value = Cat.class, name = "Cat"),
@JsonSubTypes.Type(value = Dog.class, name = "Dog")
})
public class Pet implements Serializable
But, how can we add this JsonTypeInfo to our classes? Well, via openapi's vendor extensions and open-api generator's templating, of course.
The typeInfoAnnotation.mustache file handles the annotations added to Jackson models annotated with @TypeInfo. So, this is the file we need to update. We could easily change the line for the JsonTypeInfo to use JsonTypeInfo.Id.DEDUCTION, but that would change all of our generated models to use the Deduction ability. I'm sure we would prefer to keep this more variable. So, what we can do is make use of vendor extensions to make this more variable. In your openapi specification, add a vendor extension called x-useDeduction. The value doesn't matter, but we'll go ahead and set it to true, for readability purposes.
Now your specification should look like this:
components:
schemas:
Pet:
type: object
required:
- petType
discriminator:
propertyName: petType
x-useDeduction: true
finally, in your typeInfoAnnotation.mustache file, you can set the mustache as follows:
{{#jackson}}
{{#discriminator.mappedModels}}
{{#-first}}
{{#vendorExtensions.x-useDeduction}}
@JsonTypeInfo(use = JsonTypeInfo.Id.DEDUCTION)
{{/vendorExtensions.x-useDeduction}}
{{^vendorExtensions.x-useDeduction}}
@JsonIgnoreProperties(
value = "{{{discriminator.propertyBaseName}}}", // ignore manually set {{{discriminator.propertyBaseName}}}, it will be automatically generated by Jackson during serialization
allowSetters = true // allows the {{{discriminator.propertyBaseName}}} to be set during deserialization
)
@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.PROPERTY, property = "{{{discriminator.propertyBaseName}}}", visible = true)
{{/vendorExtensions.x-useDeduction}}
@JsonSubTypes({
{{/-first}}
{{^vendorExtensions.x-discriminator-value}}
@JsonSubTypes.Type(value = {{modelName}}.class, name = "{{{mappingName}}}"){{^-last}},{{/-last}}
{{/vendorExtensions.x-discriminator-value}}
{{#vendorExtensions.x-discriminator-value}}
@JsonSubTypes.Type(value = {{modelName}}.class, name = "{{{vendorExtensions.x-discriminator-value}}}"){{^-last}},{{/-last}}
{{/vendorExtensions.x-discriminator-value}}
{{#-last}}
})
{{/-last}}
{{/discriminator.mappedModels}}
{{/jackson}}
In the mustache templating language, the #vendorExtensions.x-useDeduction tells that template to use the following lines if the property is present. Then, the ^vendorExtensions.x-useDeduction says to use the following lines if the property is not present.
Now, any object that has your new vendor extension will use JsonTypeInfo.Id.DEDUCTION instead of JsonTypeInfo.Id.NAME.
You may experience issues where the Deduction cannot determine the subtype. This can be solved by setting the default subtype in the deducer. You can do this by adjusting the mustache file to use @JsonTypeInfo(use = JsonTypeInfo.Id.DEDUCTION, defaultImpl = {{className}}.class)
Your question specifically asked how to do this via the OAS Schema. The answer is to use vendor extensions and a combination of either templating or a custom generator, depending on which language you are generating and which tools are available to you. My answer only covers a single generator as an example, but is by no means all inclusive.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With