# `Ash.Type.Union`
[🔗](https://github.com/ash-project/ash/blob/v3.23.1/lib/ash/type/union.ex#L5)

A union between multiple types, distinguished with a tag or by attempting to validate.

Union types allow you to define attributes that can hold values of different types. There are two
main strategies for distinguishing between types:

1. **Tagged unions** - Uses a specific field (tag) and value (tag_value) to identify the type
2. **Untagged unions** - Attempts to cast the value against each type in order until one succeeds

## Basic Usage

Define a union type in an attribute:

    attribute :content, :union,
      constraints: [
        types: [
          text: [type: :string],
          number: [type: :integer],
          flag: [type: :boolean]
        ]
      ]

Values are wrapped in an `%Ash.Union{}` struct with `:type` and `:value` fields:

    # Reading union values
    %Ash.Union{type: :text, value: "Hello"}
    %Ash.Union{type: :number, value: 42}

## Tagged Unions

Tagged unions use a discriminator field to identify the type. This is more reliable but requires
the data to include the tag field:

    attribute :data, :union,
      constraints: [
        types: [
          user: [
            type: :map,
            tag: :type,
            tag_value: "user"
          ],
          admin: [
            type: :map,
            tag: :type,
            tag_value: "admin"
          ]
        ]
      ]

Input data must include the tag field:

    # Valid inputs
    %{type: "user", name: "John", email: "john@example.com"}
    %{type: "admin", name: "Jane", permissions: ["read", "write"]}

### Tag Options

- `tag` - The field name to check (e.g., `:type`, `:kind`, `:__type__`)
- `tag_value` - The expected value for this type (string, atom, or nil)
- `cast_tag?` - Whether to include the tag in the final value (default: `true`)

When `cast_tag?: false`, the tag field is removed from the final value.

## Untagged Unions

Without tags, union types attempt to cast values against each type in order:

    attribute :flexible, :union,
      constraints: [
        types: [
          integer: [type: :integer],
          string: [type: :string]
        ]
      ]

**Order matters!** The first successful cast wins:

    # "42" would be cast as :integer (42), not :string ("42")
    # if integer comes first in the types list

## Mixed Tagged and Untagged Types

You can mix tagged and untagged types within a single union. Tagged types are checked first
by their tag values, and if no tagged type matches, untagged types are tried in order:

    attribute :flexible_data, :union,
      constraints: [
        types: [
          # Tagged types - checked first by tag
          user: [
            type: :map,
            tag: :type,
            tag_value: "user"
          ],
          admin: [
            type: :map,
            tag: :type,
            tag_value: "admin"
          ],
          # Untagged types - tried in order if no tag matches
          number: [type: :integer],
          text: [type: :string]
        ]
      ]

This allows for both explicit type identification (via tags) and fallback casting:

    # Tagged - uses :type field to determine it's a user
    %{type: "user", name: "John"}

    # Untagged - tries :integer first, then :string
    42        # -> %Ash.Union{type: :number, value: 42}
    "hello"   # -> %Ash.Union{type: :text, value: "hello"}

## Storage Modes

Union values can be stored in different formats:

### `:type_and_value` (default)

Stores as a map with explicit type and value fields:

    # Stored as: %{"type" => "text", "value" => "Hello"}

### `:map_with_tag`

Stores the value directly (requires all types to have tags):

    # Stored as: %{"type" => "user", "name" => "John", "email" => "john@example.com"}

    constraints: [
      storage: :map_with_tag,
      types: [
        user: [type: :map, tag: :type, tag_value: "user"],
        admin: [type: :map, tag: :type, tag_value: "admin"]
      ]
    ]

## Embedded Resources

Union types work seamlessly with embedded resources:

    attribute :contact_info, :union,
      constraints: [
        types: [
          email: [
            type: EmailContact,
            tag: :type,
            tag_value: "email"
          ],
          phone: [
            type: PhoneContact,
            tag: :type,
            tag_value: "phone"
          ]
        ]
      ]

## Arrays of Unions

Union types support arrays using the standard `{:array, :union}` syntax:

    attribute :mixed_data, {:array, :union},
      constraints: [
        items: [
          types: [
            text: [type: :string],
            number: [type: :integer]
          ]
        ]
      ]

## Advanced Input Formats

Union types support multiple input formats for flexibility:

### Direct Union Struct

    %Ash.Union{type: :text, value: "Hello"}

### Tagged Map (when tags are configured)

    %{type: "text", content: "Hello"}

### Explicit Union Format

    %{
      "_union_type" => "text",
      "_union_value" => "Hello"
    }

    # Or with the value merged in
    %{
      "_union_type" => "text",
      "content" => "Hello"
    }

## Nested Unions

Unions can contain other union types. All type names must be unique across the entire
nested structure:

    types: [
      simple: [type: :string],
      complex: [
        type: :union,
        constraints: [
          types: [
            # Names must be unique - can't reuse 'simple'
            nested_text: [type: :string],
            nested_num: [type: :integer]
          ]
        ]
      ]
    ]

## Loading and Calculations

Union types support loading related data when member types are loadable (like embedded resources):

    # Load through all union types
    query |> Ash.Query.load(union_field: :*)

    # Load through specific type
    query |> Ash.Query.load(union_field: [user: [:profile, :preferences]])

## Error Handling

Union casting provides detailed error information:

- **Tagged unions**: Clear errors when tags don't match expected values
- **Untagged unions**: Aggregated errors from all attempted type casts
- **Array unions**: Errors include index and path information for debugging

## NewType Integration

Create reusable union types with `Ash.Type.NewType`:

    defmodule MyApp.Types.ContactMethod do
      use Ash.Type.NewType,
        subtype_of: :union,
        constraints: [
          types: [
            email: [type: :string, constraints: [match: ~r/@/]],
            phone: [type: :string, constraints: [match: ~r/^+$/]]
          ]
        ]
    end

## Performance Considerations

- **Tagged unions** are more efficient as they avoid trial-and-error casting
- **Type order matters** in untagged unions - put more specific types first
- **Use constraints** on member types to fail fast on invalid data

## Constraints

* `:storage` - How the value will be stored when persisted.  
  `:type_and_value` will store the type and value in a map like so `{type: :type_name, value: the_value}`
  `:map_with_tag` will store the value directly. This only works if all types have a `tag` and `tag_value` configured.
   Valid values are :type_and_value, :map_with_tag The default value is `:type_and_value`.

* `:include_source?` (`t:boolean/0`) - Whether to include the source changeset in the context. Defaults to the value of `config :ash, :include_embedded_source_by_default`, or `true`. In 4.x, the default will be `false`. The default value is `true`.

* `:types` - The types to be unioned, a map of an identifier for the enum value to its configuration.  
  When using `tag` and `tag_value` we are referring to a map key that must equal a certain value
  in order for the value to be considered an instance of that type.  
  For example:  
      types:  [
        int: [
          type: :integer,
          constraints: [
            max: 10
          ]
        ],
        object: [
          type: MyObjectType,
          # The default value is `true`
          # this passes the tag key/value to the nested type
          # when casting input
          cast_tag?: true,
          tag: :type,
          tag_value: "my_object"
        ],
        other_object: [
          type: MyOtherObjectType,
          cast_tag?: true,
          tag: :type,
          tag_value: "my_other_object"
        ],
        other_object_without_type: [
          type: MyOtherObjectTypeWithoutType,
          cast_tag?: false,
          tag: :type,
          tag_value: nil
        ]
      ]  
  IMPORTANT:  
  This is stored as a map under the hood. Filters over the data will need to take this into account.  
  Additionally, if you are not using a tag, a value will be considered to be of the given type if it successfully casts.
  This means that, for example, if you try to cast `"10"` as a union of a string and an integer, it will end up as `"10"` because
  it is a string. If you put the integer type ahead of the string type, it will cast first and `10` will be the value.

# `handle_change?`

# `prepare_change?`

---

*Consult [api-reference.md](api-reference.md) for complete listing*
