Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Include tag_value as discriminator attribute when writing Ash.Type.Union to OpenAPI #272

Open
deerob4 opened this issue Jan 2, 2025 · 1 comment
Labels
enhancement New feature or request

Comments

@deerob4
Copy link
Contributor

deerob4 commented Jan 2, 2025

It would be really useful to include more information about union type variants inside the OpenAPI output.

Say that I declare these types:

defmodule Circle do
  use Ash.Resource, data_layer: :embedded

  attributes do
    attribute :radius, :integer, public?: true, allow_nil?: false
  end
end

defmodule Square do
  use Ash.Resource, data_layer: :embedded

  attributes do
    attribute :length, :integer, public?: true, allow_nil?: false
  end
end

defmodule EquilateralTriangle do
  use Ash.Resource, data_layer: :embedded

  attributes do
    attribute :length, :integer, public?: true, allow_nil?: false
  end
end

defmodule Shape do
  use Ash.Type.NewType,
    subtype_of: :union,
    constraints: [
      types: [
        circle: [
          type: Circle,
          tag: :type,
          tag_value: :circle
        ],
        square: [
          type: Square,
          tag: :type,
          tag_value: :square
        ],
        equilaterial_triangle: [
          type: EquilateralTriangle,
          tag: :type,
          tag_value: :triangle
        ],
      ]
    ]
end

As things stand today, the output will be this:

{
  "shape": {
    "anyOf": [
      {
        "properties": {
          "length": {
            "description": "Field included by default.",
            "type": "integer"
          }
        },
        "required": [
          "length"
        ],
        "type": "object"
      },
      {
        "properties": {
          "radius": {
            "description": "Field included by default.",
            "type": "integer"
          }
        },
        "required": [
          "radius"
        ],
        "type": "object"
      }
    ],
    "description": "Field included by default."
  }
}

Notice how it doesn't include the tag_value property defined in the embedded resources, and how Square and EquilaterialTriangle are merged into the same object due to having the same fields.

This is a bit unfortunate, since it throws away information about which variant the data comes from. This is useful information to have when using the OpenAPI specification to generate e.g. TypeScript or Swift types, since these languages can use the discriminator field to narrow down the current variant.

Ideally, I'd like to see this output instead:

{
  "schemas": {
    "Shape": {
      "anyOf": [
        {
          "$ref": "#/components/schemas/Circle"
        },
        {
          "$ref": "#/components/schemas/Square"
        },
        {
          "$ref": "#/components/schemas/EquilateralTriangle"
        }
      ],
      "discriminator": {
        "propertyName": "type",
        "mapping": {
          "circle": "#/components/schemas/Circle",
          "square": "#/components/schemas/Square",
          "triangle": "#/components/schemas/EquilateralTriangle",
        }
      }
    },
    "Circle": {
      "type": "object",
      "required": ["type", "radius"],
      "properties": {
        "type": {
          "type": "string",
          "enum": ["circle"]
        },
        "radius": {
          "type": "integer"
        }
      },
    },
    "Square": {
      "type": "object",
      "required": ["type", "length"],
      "properties": {
        "type": {
          "type": "string",
          "enum": ["square"]
        },
        "length": {
          "type": "integer"
        }
      } 
    },
    "EquilateralTriangle": {
      "type": "object",
      "required": ["type", "length"],
      "properties": {
        "type": {
          "type": "string",
          "enum": ["triangle"]
        },
        "length": {
          "type": "integer"
        }
      } 
    },
  }
}

In this version, the union type has a "discriminator" field, and how "type" has been added as a single enum property to each variant type.

Is this something you think could be useful? I'm happy to have a go at implementing it and making a PR.

@deerob4 deerob4 added enhancement New feature or request needs review labels Jan 2, 2025
@zachdaniel
Copy link
Contributor

It makes sense to set a discriminator on unions where they all share a tag, yeah. We also need to set the tag and tag value into the map while serializing in order for it to be exposed given resources like yours that don't store their type.

Sounds like a good change to me, and like it should be non-breaking.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement New feature or request
Projects
None yet
Development

No branches or pull requests

2 participants