OpenAPI / Swagger 3.0: Default discriminator value
Asked Answered
D

2

12

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?

Distraction answered 3/8, 2020 at 22:6 Comment(2)
Did you figure it out?Ertha
Facing the same problem. Has anyone found possible solution?Turnery
R
2

OpenApi Specification

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).

OpenApi Generator

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.

Alternatives

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.

Templating and vendor-extensions. What a combo!

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.

Further Customization

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)

Disclaimer

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.

Rolf answered 16/1, 2023 at 20:41 Comment(1)
This doesn't work. At least, not in the way described. vendorExtensions does not have any populated values on typeInfoAnnotation.mustache. vendorExtensions is available however in the file additionalModelTypeAnnotations.mustache. I had to get around this by putting @JsonTypeInfo(use = JsonTypeInfo.Id.DEDUCTION, defaultImpl = {{classFilename}}.class) into the additionalModelTypeAnnotations.mustache file and completely removing the existing JsonTypeInfo from typeInfoAnnotation.mustache. Which isn't ideal but works for me for nowTarpaulin
C
0

The default value is maybe implicit as soon as a mapping is specified ????

discriminator:
        propertyName: petType
        mapping:
          dog: Dog
          cat: Cat
Chatelaine answered 16/5, 2022 at 19:22 Comment(0)

© 2022 - 2025 — McMap. All rights reserved.