Skip to content

Avoid generating intermediate structs if oneOf value is a $ref #358

@lgfa29

Description

@lgfa29

Overview

Some endpoints have a spec structure where an oneOf variant value is hoisted up.

For example, InstanceNetworkInterfaceCreate has the following schema:

"InstanceNetworkInterfaceCreate": {
  "description": "Create-time parameters for an `InstanceNetworkInterface`",
  "type": "object",
  "properties": {
    "description": {
      "type": "string"
    },
    "ip_config": {
      "description": "The IP stack configuration...",
      "allOf": [
        {
          "$ref": "#/components/schemas/PrivateIpStackCreate"
        }
      ]
    },
    ...
  }
}
...
"PrivateIpStackCreate": {
  "description": "Create parameters for a network interface's IP stack.",
  "oneOf": [
    {
      "description": "The interface has only an IPv4 stack.",
      "type": "object",
      "properties": {
        "type": {
          "type": "string",
          "enum": [
            "v4"
          ]
        },
        "value": {
          "$ref": "#/components/schemas/PrivateIpv4StackCreate"
        }
      },
      "required": [
        "type",
        "value"
      ]
    },
    ...
  ]
}
...
"PrivateIpv4StackCreate": {
  "description": "Configuration for a network interface's IPv4 addressing.",
  "type": "object",
  "properties": {
    "ip": {
      "description": "The VPC-private address to assign to the interface.",
      "allOf": [
        {
          "$ref": "#/components/schemas/Ipv4Assignment"
        }
      ]
    },
    "transit_ips": {
      "description": "Additional IP networks the interface can send / receive on.",
      "default": [],
      "type": "array",
      "items": {
        "$ref": "#/components/schemas/Ipv4Net"
      }
    }
  },
  "required": [
    "ip"
  ]
}

With this spec, the JSON structure expected by the API is:

{
  "type": "v4",
  "value": {
    "ip": {
      "type": "auto"
    },
    "transit_ips": []
  }
}

For this spec, the Go SDK generates the following structs:

type InstanceNetworkInterfaceCreate struct {
	// ...
	IpConfig PrivateIpStackCreate `json:"ip_config,omitempty" yaml:"ip_config,omitempty"`
	// ...
}

type PrivateIpStackCreate struct {
	Type PrivateIpStackCreateType `json:"type,omitempty" yaml:"type,omitempty"`
	Value any `json:"value,omitempty" yaml:"value,omitempty"`
}

type PrivateIpStackCreateV4 struct {
	Type PrivateIpStackCreateType `json:"type" yaml:"type"`
	Value PrivateIpv4StackCreate `json:"value" yaml:"value"`
}

type PrivateIpv4StackCreate struct {
	Ip Ipv4Assignment `json:"ip" yaml:"ip"`
	TransitIps []Ipv4Net `json:"transit_ips,omitempty" yaml:"transit_ips,omitempty"`
}

A Go SDK user would reasonably expect to build a struct like this to make this request:

netIfCreate := oxide.InstanceNetworkInterfaceCreate{
	IpConfig: oxide.PrivateIpStackCreate{
		Type: oxide.PrivateIpStackCreateTypeV4,
		Value: oxide.PrivateIpStackCreateV4{
			Type: oxide.PrivateIpStackCreateTypeV4,
			Value: oxide.PrivateIpv4StackCreate{
				Ip: oxide.Ipv4Assignment{/* ... */},
			},
		},
	},
}

But this creates a JSON with an unnecessary additional nesting:

  {
    "type": "v4",
    "value": {
-      "value": {
        "ip": {
          "type": "auto"
        },
        "transit_ips": []
-      }
    }
  }

A similar issue happens when parsing API responses.

type InstanceNetworkInterface struct {
	// ...
	IpStack PrivateIpStack `json:"ip_stack" yaml:"ip_stack"`
	// ...
}

type PrivateIpStack struct {
	Type PrivateIpStackType `json:"type,omitempty" yaml:"type,omitempty"`
	Value any `json:"value,omitempty" yaml:"value,omitempty"`
}

type PrivateIpStackV4 struct {
	Type PrivateIpStackType `json:"type" yaml:"type"`
	Value PrivateIpv4Stack `json:"value" yaml:"value"`
}

type PrivateIpv4Stack struct {
	Ip string `json:"ip" yaml:"ip"`
	TransitIps []Ipv4Net `json:"transit_ips" yaml:"transit_ips"`
}

Users need to unmarshal the returned JSON into a PrivateIpv4Stack instead of PrivateIpStackV4:

switch stack.Type {
case oxide.PrivateIpStackTypeV4:
	// Correct.
	var parsedStack oxide.PrivateIpv4Stack
	if err := json.Unmarshal(marshalled, &parsedStack); err != nil {
		// ...
	}

	// Incorrect.
	var parsedStack oxide.PrivateIpStackV4
	if err := json.Unmarshal(marshalled, &parsedStack); err != nil {
		// ...
	}
// ...

So effectively PrivateIpStackCreateV4 and PrivateIpStackV4 should never be used, but this is not clear from the generate Go code. The names os the structs that should and should not be used being so similar, and the use of any to represent enums, add a little more to the confusion.

Implementation details

No response

Anything else you would like to add?

No response

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions