Kubus
Kubus

Reputation: 777

How to deserialize an OpenAPI 3 YAML file with a polymorphic array based on a discriminator in Golang?

I would like to deserialize some YAML data in Golang based on an OpenApi 3 schema. The specification contains a polymorphic array. How can it be deserialized to the corresponding struct types in Golang based on a discriminator type? I tried to use an interface as a base type in the Traits array, but then I'm not able to upcast the individual items with an explicit cast. Is this an issue of the go-yaml library I'm using or in the structure of my structs / interface types?

Here is my crude attempt:


import (
  "fmt"
  "github.com/goccy/go-yaml"
  //"gopkg.in/yaml.v3"
)

func main() {
    discriminators()
}

func discriminators() {
    var yamlData = `
spec:
  traits:
    - name: Web dashboard
      type: ui
      summary: hmmm ET
    - name: Wiki
      type: documentation
      description: Description
`
    data := Data{}

    if err := yaml.Unmarshal([]byte(yamlData), &data); err == nil {

        trait := data.Spec.Traits[0]

        //type=ui
        uiTrait := trait.(UiTrait)

        println(uiTrait.Description)
    }
}

type TraitInterface interface {
    getName() string //`yaml:"name"`
}

type Trait struct {
    Name  string `yaml:"name"`
    Type_ string `yaml:"type"`
}

type UiTrait struct {
    //Trait Trait `json:"trait"`

    Name  string `yaml:"name"`
    Type_ string `yaml:"type"`

    Description string `json:"description,omitempty"`
}

type DocumentationTrait struct {
    //Trait Trait `json:"trait"`

    Name  string `yaml:"name"`
    Type_ string `yaml:"type"`

    Summary string `json:"summary,omitempty"`
}

type Specification struct {
    Traits []interface{} `yaml:"traits,omitempty"`
    //Traits []TraitInterface `yaml:"traits,omitempty"`
}

type Data struct {
    Spec Specification `yaml:"spec"`
}

func (a UiTrait) getName() string {

    return a.Name
}

OpenApi 3 specification with oneOf:

openapi: "3.0.0"

components:

  schemas:

    Specification:
      type: object
      additionalProperties: false
      properties:
        traits:
          type: array
          items:
            oneOf:
            - $ref: "#/components/schemas/UiTrait"
            - $ref: "#/components/schemas/DocumentationTrait"
            discriminator: 
              propertyName: type
              mapping:
                ui: "#/components/schemas/UiTrait"
                documentation: "#/components/schemas/DocumentationTrait"  

    Trait:
      type: object
      additionalProperties: false
      properties:
        name:
          type: string
        type:
          description: Type of trait
          type: string
        description:
          description: Description
          type: string
      required:
      - name
      - type

    DocumentationTrait:
      allOf:
      - $ref: "#/components/schemas/Trait"
      - type: object
        additionalProperties: false
        properties:
          summary:
            type: string

    UiTrait:
      allOf:
      - $ref: "#/components/schemas/Trait"
      - type: object
        additionalProperties: false
        properties:
          description:
            type: string

Thank you!

Upvotes: 1

Views: 1492

Answers (1)

The Fool
The Fool

Reputation: 20467

The type of the items is map[string] interface{}, that's why the type assertion fails. It's not a conversion. It just checks (asserts) if the interface is actually that type. See https://go.dev/tour/methods/15.

I am suggesting to use mapstructure which has been created with this usecase in mind.

Perhaps we can't populate a specific structure without first reading the "type" field from the JSON. We could always do two passes over the decoding of the JSON (reading the "type" first, and the rest later). However, it is much simpler to just decode this into a map[string]interface{} structure, read the "type" key, then use something like this library to decode it into the proper structure.

Also note that you have a bug in your types. The UI type has a summary and the documentation type as a description, but in your code it's switched around.

func discriminators() {
    var yamlData = `
spec:
  traits:
    - name: Web dashboard
      type: ui
      summary: hmmm ET
    - name: Wiki
      type: documentation
      description: Description
`
    data := Data{}

    err := yaml.Unmarshal([]byte(yamlData), &data)
    if err != nil {
        panic(err)
    }
    trait := data.Spec.Traits[0]
    var ut UiTrait
    if err := mapstructure.Decode(trait, &ut); err != nil {
        panic(err)
    }
    println(ut.Summary)

}

type Data struct {
    Spec Specification `yaml:"spec"`
}

type Trait struct {
    Name  string `yaml:"name"`
    Type_ string `yaml:"type"`
}

type UiTrait struct {
    Name    string `yaml:"name"`
    Type_   string `yaml:"type"`
    Summary string `json:"summary,omitempty"`
}

type DocumentationTrait struct {
    Name        string `yaml:"name"`
    Type_       string `yaml:"type"`
    Description string `json:"description,omitempty"`
}

type Specification struct {
    Traits []interface{} `yaml:"traits,omitempty"`
}

https://go.dev/play/p/ZjrEzn6jEYQ

Upvotes: 1

Related Questions