Unions

Unions allow you to validate data that can be one of several different types. Zangar provides powerful union validation through the | operator, enabling flexible data validation while maintaining type safety.

Basic Unions

The simplest way to create a union is using the | operator between two schemas:

import zangar as z

# A value that can be either an integer or a string
int_or_str = z.int() | z.str()

# Parse different types
assert int_or_str.parse(42) == 42
assert int_or_str.parse("hello") == "hello"

# Validation fails if neither type matches
try:
    int_or_str.parse([1, 2, 3])  # list is neither int nor str
except z.ValidationError as e:
    print(e.format_errors())
    # [
    #     {'msgs': ['Expected int, received list']},
    #     {'msgs': ['Expected str, received list']}
    # ]

Multiple Unions

You can chain multiple types together to create unions with more than two options:

# A value that can be int, str, or bool
multi_type = z.int() | z.str() | z.bool()

assert multi_type.parse(42) == 42
assert multi_type.parse("hello") == "hello"
assert multi_type.parse(True) == True

Union with None (Optional Values)

A common pattern is to make a value optional by unioning it with None:

# Optional integer
optional_int = z.int() | z.none()

assert optional_int.parse(42) == 42
assert optional_int.parse(None) is None

# Optional string with validation
optional_name = z.str().min(2) | z.none()

assert optional_name.parse("John") == "John"
assert optional_name.parse(None) is None

try:
    optional_name.parse("A")  # Too short
except z.ValidationError:
    pass  # Validation fails

Complex Unions

Unions can include complex schemas like structures and lists:

# Union of different data structures
user_data = z.struct({
    'name': z.str(),
    'age': z.int()
}) | z.struct({
    'username': z.str(),
    'email': z.str()
})

# Can parse either structure
user1 = user_data.parse({'name': 'John', 'age': 30})
user2 = user_data.parse({'username': 'john_doe', 'email': '[email protected]'})

# Union with lists
list_or_single = z.list(z.str()) | z.str()

assert list_or_single.parse(['a', 'b', 'c']) == ['a', 'b', 'c']
assert list_or_single.parse('single') == 'single'

Validation Order and Error Handling

Zangar tries each schema in the union from left to right. If all schemas fail, it collects all validation errors:

schema = z.int().gt(0) | z.str().min(5)

# First schema succeeds
assert schema.parse(10) == 10

# Second schema succeeds
assert schema.parse("hello world") == "hello world"

# Both schemas fail - shows all errors
try:
    schema.parse(-5)  # Negative int fails first schema
except z.ValidationError as e:
    print(e.format_errors())
    # [
    #     {'msgs': ['The value should be greater than 0']},
    #     {'msgs': ['Expected str, received int']}
    # ]

try:
    schema.parse("hi")  # Short string fails both schemas
except z.ValidationError as e:
    print(e.format_errors())
    # [
    #     {'msgs': ['Expected int, received str']},
    #     {'msgs': ['The value should be at least 5 characters long']}
    # ]

Transformations with Unions

You can apply transformations to unions, which will be applied after successful validation:

# Transform the result regardless of which type matched
schema = (z.int() | z.str()).transform(lambda x: f"Value: {x}")

assert schema.parse(42) == "Value: 42"
assert schema.parse("hello") == "Value: hello"

# Transform only affects successful parsing
number_or_default = (z.int() | z.transform(lambda _: 0))

assert number_or_default.parse(42) == 42
assert number_or_default.parse("anything") == 0