Search code examples
javaswaggerswagger-codegenopenapi-generator

Swagger generate subtypes based on enum value


I have the following structure

             Notification
                  |
        ------------------------
        |                      |
  SmsNotification         EmailNotification

The Notification contains an enum notificationType containing either SMS or EMAIL. Now I have an Inbox class, which contains a Notification.

This is specified in the swagger yml as such (removed some irrelevant code)

definitions:
  Notification:
    type: "object"
    discriminator: "notificationType"
    properties:
      notificationType:
        type: "string"
        description: "Type of notification"
        enum:
          - "EMAIL"
          - "SMS"

  SmsNotification:
    allOf:
      - $ref: "#/definitions/Notification"
      - type: "object"

  EmailNotification
    allOf:
      - $ref: "#/definitions/Notification"
      - type: "object"

  Inbox:
    type: "object"
    properties:
      notification:
        description: "Latest received notification"
        $ref: "#/definitions/Notification"

I generate my code with swagger-codegen v2 (tried v3 & openapi-generator as well) with the following configuration:

<build>
  <plugins>
      <plugin>
          <groupId>io.swagger</groupId>
          <artifactId>swagger-codegen-maven-plugin</artifactId>
          <version>2.3.1</version>
          <executions>
              <execution>
                 <id>notifications</id>
                  <goals>
                      <goal>generate</goal>
                  </goals>
                  <configuration>
                      <inputSpec>${project.basedir}/src/main/notifications/swagger.yaml</inputSpec>
                      <language>java</language>
                      <library>jersey2</library>
                      <generateSupportingFiles>false</generateSupportingFiles>
                      <modelPackage>${generated.package}</modelPackage>
                      <generateApis>false</generateApis>
                      <generateApiDocumentation>false</generateApiDocumentation>
                      <generateModelTests>false</generateModelTests>
                      <generateModelDocumentation>false</generateModelDocumentation>
                  </configuration>
              </execution>
         </executions>
     </plugin>
  </plugins>
</build>

Now what happens is that the jersey2 library will generate JsonSubType annotations as such:

@JsonTypeInfo(use=JsonTypeInfo.Id.NAME, include=JsonTypeInfo.As.Property, property="notificationType", visible=true)
@JsonSubTypes({
  @JsonSubTypes.Type(value=SmsNotification.class, name="SmsNotification"),
  @JsonSubTypes.Type(value=EmailNotification.class, name="EmailNotification")
})
public class Notification {
  ...
}

The problem here is that if I now try to deserialize/serialize a Json string containing an Inbox with the notificationType=EMAIL, that it will throw an exception since there is no known subtype with the name 'EMAIL'.

The seralizer expects the JsonSubType annotations to be specified like this: (sidenote, this is also how the code looks from which the swagger yaml is generated)

@JsonTypeInfo(use=JsonTypeInfo.Id.NAME, include=JsonTypeInfo.As.Property, property="notificationType", visible=true)
@JsonSubTypes({
  @JsonSubTypes.Type(value=SmsNotification.class, name="SMS"),
  @JsonSubTypes.Type(value=EmailNotification.class, name="EMAIL")
})
public class Notification {
  ...
}

Does anyone know how to generate the JsonSubTypes annotation as desired instead of the current behaviour?


Solution

  • I had a similar issue and fixed it.

    I do not maintain the OpenAPI definition directly but use annotations in my beans. Then I generate the OpenAPI definition (JSON format) and then I generate the client project using openapi-generator.

    The issue came from the missing DiscriminatorMapping annotations.


    Working example

    Java beans

    Entity.java

    @JsonTypeInfo(
        use = JsonTypeInfo.Id.NAME,
        include = JsonTypeInfo.As.EXISTING_PROPERTY,
        property = "type",
        visible = true
    )
    @JsonSubTypes({
        @JsonSubTypes.Type(value = FooEntity.class, name = "FOO"),
        @JsonSubTypes.Type(value = BarEntity.class, name = "BAR")
    })
    // Without the following annotations I have the same issue than OP
    @Schema(
        description = "Entity",
        discriminatorProperty = "type",
        discriminatorMapping = {
            @DiscriminatorMapping(schema = FooEntity.class, value = "FOO"),
            @DiscriminatorMapping(schema = BarEntity.class, value = "BAR")
        }
    )
    public abstract class Entity {
    
        @Schema(description = "Entity type", required = true)
        protected final EntityType type;
    }
    

    EntityType.java

    @Schema(description = "Entity type")
    public enum EntityType {
        FOO,
        BAR
    }
    

    FooEntity.java (BarEntity.java works the same way).

    @Schema(description = "Foo entity")
    public class FooEntity extends Entity {
    
        public FooEntity() {
            super(EntityType.FOO);
        }
    }
    

    Definition file

    // ...
    "schemas": {
        "Entity": {
            "required": [
                "type"
            ],
            "type": "object",
            "properties": {
                "type": {
                    "type": "string",
                    "enum": [
                        "FOO",
                        "BAR"
                    ]
                }
            },
            "description": "Entity",
            "discriminator": {
                "propertyName": "type",
                "mapping": {
                    "FOO": "#/components/schemas/FooEntity",
                    "BAR": "#/components/schemas/BarEntity"
                }
            }
        },
        "FooEntity": {
            "required": [
                "type"
            ],
            "type": "object",
            "description": "Foo entity",
            "allOf": [
                {
                    "$ref": "#/components/schemas/Entity"
                }
            ]
        },
    }
    // ...
    

    Generated using the following plugin in my pom.xml

    <plugin>
        <groupId>io.swagger.core.v3</groupId>
        <artifactId>swagger-maven-plugin</artifactId>
        <version>2.1.2</version>
        <executions>
            <execution>
                <id>generate-api-definition</id>
                <phase>compile</phase>
                <goals>
                    <goal>resolve</goal>
                </goals>
            </execution>
        </executions>
        <configuration>
            <configurationFilePath>${project.build.directory}/api/WEB-INF/openapi-configuration.json</configurationFilePath> <!-- My configuration file -->
            <outputPath>${project.build.directory}/api/WEB-INF</outputPath>
            <outputFileName>openapi</outputFileName>
            <outputFormat>JSON</outputFormat>
        </configuration>
    </plugin>
    

    Generated client

    //...
    @javax.annotation.Generated(value = "org.openapitools.codegen.languages.JavaClientCodegen", date = "2020-05-07T15:17:15.844882+02:00[Europe/Paris]")
    @JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.EXISTING_PROPERTY, property = "type", visible = true)
    @JsonSubTypes({
        @JsonSubTypes.Type(value = FooEntity.class, name = "FOO"),
        @JsonSubTypes.Type(value = BarEntity.class, name = "BAR")
    })
    public class Entity {
    
        public enum TypeEnum {
            FOO("FOO"),
            BAR("BAR");
            // ...
        }
    
        public static final String JSON_PROPERTY_TYPE = "type";
        protected TypeEnum type;
    
        public Entity type(TypeEnum type) {
            this.type = type;
            return this;
        }
    
        @ApiModelProperty(required = true, value = "Entity type")
        @JsonProperty(JSON_PROPERTY_TYPE)
        @JsonInclude(value = JsonInclude.Include.ALWAYS)
    
        public TypeEnum getType() {
            return type;
        }
    
    
        public void setType(TypeEnum type) {
            this.type = type;
        }
        // ...
    }
    

    Generator plugin :

    <plugin>
        <groupId>org.openapitools</groupId>
        <artifactId>openapi-generator-maven-plugin</artifactId>
        <version>4.3.1</version>
        <executions>
            <execution>
                <id>generate-client</id>
                <phase>generate-sources</phase>
                <goals>
                    <goal>generate</goal>
                </goals>
                <configuration>
                    <!-- https://github.com/OpenAPITools/openapi-generator/tree/master/modules/openapi-generator-maven-plugin -->
                    <inputSpec>x/y/z/openapi.json</inputSpec>
                    <addCompileSourceRoot>false</addCompileSourceRoot>
                    <generatorName>java</generatorName>
                    <configOptions>
                        <java8>true</java8>
                        <serializableModel>true</serializableModel>
                        <serializationLibrary>jackson</serializationLibrary>
                        <library>jersey2</library>
                        <dateLibrary>java8</dateLibrary>
                        <!-- ... -->
                    </configOptions>
                </configuration>
            </execution>
        </executions>
    </plugin>