Search code examples
javatypescriptopenapiopenapi-generator

Using OpenAPI `oneOf`: generate TypeScript code that can parse JavaSpring API?


I'm using openapi-generator 6.2.1 for generating both server-side Java Spring interfaces and TypeScript client classes from openapi 3.0.2 YAML files.

I'm trying to use the oneOf functionality to specify that a particular property can have one of two exact types: PublicMetadataSchemaV1 or ClosedMetadataSchemaV1.

Here's the definition:

    ReadRaidMetadataResponseV1:
      # Using oneOf/discriminator is probably pushing too close to the edge of 
      # what openapi-gen can do yet, for example that's why metadataSchema is a
      # string instead of an enum:
      # https://github.com/OpenAPITools/openapi-generator/pull/13846
      type: object
      description: Any type of metadata
      oneOf:
#        - $ref: 'metadata-schema-v1.yaml#/components/schemas/MetadataSchemaV1'
        - $ref: '#/components/schemas/PublicMetadataSchemaV1'
        - $ref: '#/components/schemas/ClosedMetadataSchemaV1'
      discriminator:
        propertyName: metadataSchema
        mapping:
          raido-metadata-schema-v1: '#/components/schemas/PublicMetadataSchemaV1'
          closed-metadata-schema-v1: '#/components/schemas/ClosedMetadataSchemaV1'
    PublicMetadataSchemaV1:
      description: >
        This object only exists because openapi-gen discriminator needs to be 
        a string, currently.
        See https://github.com/OpenAPITools/openapi-generator/pull/13846.
        Eventually, want the mapping to just use MetadataSchemaV1.
      type: object
      required: [ metadataSchema, id, titles, dates, access]
      # this is how we make PublicMetadataSchemaV1 inherit all the fields oneOf: 
      # MetadataSchemaV1.
      allOf: 
        - $ref: 'metadata-schema-v1.yaml#/components/schemas/MetadataSchemaV1'
      properties:
        # This is where we "override" the type of metadataSchema to be a string
        # instead of the enum that we would prefer it to be.
        # Rather than "string" this should be a "constant" with value `raido-metadata-schema-v1`
        # metadataSchema: {$ref: 'shared.yaml#/components/schemas/RaidoMetaschema' }
        metadataSchema: { type: string }
    ClosedMetadataSchemaV1:
      type: object
      required: [ metadataSchema, id, titles, dates, access]
      properties:
        # Rather than "string" this should be a "constant" with value `closed-metadata-schema-v1`
        # metadataSchema: {$ref: 'shared.yaml#/components/schemas/RaidoMetaschema' }
        metadataSchema: { type: string }
        id: {$ref: 'shared.yaml#/components/schemas/IdBlock'}
        access: {$ref: 'shared.yaml#/components/schemas/AccessBlock'}
 

The problem here is that the JavaSpring generator creates code that looks like:

@JsonIgnoreProperties(
  value = "metadataSchema", // ignore manually set metadataSchema, it will be automatically generated by Jackson during serialization
  allowSetters = true // allows the metadataSchema to be set during deserialization
)
@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.PROPERTY, property = "metadataSchema", visible = true)
@JsonSubTypes({
  @JsonSubTypes.Type(value = ClosedMetadataSchemaV1.class, name = "ClosedMetadataSchemaV1"),
  @JsonSubTypes.Type(value = PublicMetadataSchemaV1.class, name = "PublicMetadataSchemaV1"),
  @JsonSubTypes.Type(value = ClosedMetadataSchemaV1.class, name = "closed-metadata-schema-v1"),
  @JsonSubTypes.Type(value = PublicMetadataSchemaV1.class, name = "raido-metadata-schema-v1")
})

@Generated(value = "org.openapitools.codegen.languages.SpringCodegen", date = "2022-11-21T15:03:48.931461200+10:00[Australia/Brisbane]")
public interface ReadRaidMetadataResponseV1 {
    public String getMetadataSchema();
}

And the API service always serves the "class name" mapping (i.e. PublicMetadataSchemaV1) as the value instead of the defined "mapping" value of raido-metadata-schema-v1.

But the generated TypeScript code looks like:

export function ReadRaidMetadataResponseV1FromJSONTyped(json: any, ignoreDiscriminator: boolean): ReadRaidMetadataResponseV1 {
    if ((json === undefined) || (json === null)) {
        return json;
    }
    switch (json['metadataSchema']) {
        case 'closed-metadata-schema-v1':
            return {...ClosedMetadataSchemaV1FromJSONTyped(json, true), metadataSchema: 'closed-metadata-schema-v1'};
        case 'raido-metadata-schema-v1':
            return {...PublicMetadataSchemaV1FromJSONTyped(json, true), metadataSchema: 'raido-metadata-schema-v1'};
        default:
            throw new Error(`No variant of ReadRaidMetadataResponseV1 exists with 'metadataSchema=${json['metadataSchema']}'`);
    }
}

So it always fails, because the TypeScript code only knows about the (correct) raido-metadata-schema-v1 mapping value.

The repo with the full code is publicly visible, you can find the full openapi YAML files at: https://github.com/au-research/raido-v2/blob/b06c08349a58b4559768963c5e3fbb81cc2c2f28/api-svc/idl-raid-v2/src/shared.yaml#L36

The question: How can I structure my openapi definition, or what flags do I need to use so that the generated typescript-fetch code can actually use the API served by the JavaSpring generator?


Solution

  • The easy "workaround" answer (*) to this question is that you must set the mapping value be the same as the name of the type that is being mapped to. That is:

          discriminator:
            propertyName: metadataSchema
            mapping:
              raido-metadata-schema-v1: '#/components/schemas/PublicMetadataSchemaV1'
              closed-metadata-schema-v1: '#/components/schemas/ClosedMetadataSchemaV1'
    
    

    has to be re-written as:

          discriminator:
            propertyName: metadataSchema
            mapping:
              PublicMetadataSchemaV1: '#/components/schemas/PublicMetadataSchemaV1'
              ClosedMetadataSchemaV1: '#/components/schemas/ClosedMetadataSchemaV1'
    

    (*) - "Easy" but annoying and potentially awkward. If anyone posts an answer that allows the mapping values to be arbitrary instead of being forced to match the type name - I will gladly mark it as the correct answer.