BoundaryML/baml · error · ConvertError

Unions must be flattened

Error message

Unions must be flattened

What it means

ConvertError::UnflattenedUnion is raised in convert_ty when a SapTy::Union contains a member that is itself union-like — a nested union or a type alias that resolves to a union. The SAP model representation requires unions to be fully flattened: members must not be unions or optional, and aliases resolving to unions are not permitted.

Source

Thrown at baml_language/crates/bex_sap/src/sap_model/convert.rs:44

    #[error("Unknown media kind")]
    UnknownMediaKind,
    #[error("Float literals cannot be parsed")]
    FloatLiteral,
    #[error("Non-parsable type: {0:?}")]
    NonParsableType(Box<SapTy>),
    #[error("Unknown class: {0}")]
    UnknownClass(DefKey),
    #[error("Unknown enum: {0}")]
    UnknownEnum(DefKey),
    #[error("Unknown type alias: {0}")]
    UnknownTypeAlias(DefKey),
    #[error("Unknown name (could not determine if it was a class, enum, or type alias): {0}")]
    UnknownName(DefKey),
    #[error("Could not add a type to the database as the name `{0}` is already present")]
    AlreadyPresent(DefKey),
    #[error("Recursion depth exceeded for {0}")]
    RecursionDepthExceeded(&'static str),
    #[error("Unions must be flattened")]
    UnflattenedUnion,
    /// Something like `type A = B; type B = A;` is invalid.
    #[error("Recursive type alias without indirection: {0}")]
    DirectRecursiveTypeAlias(DefKey),
    #[error("Internal error (please report): {0}")]
    InternalError(&'static str),
}

const MAX_RECURSION_DEPTH: usize = 16;

/// Contains stuff from [`sys_types::SysOpContext`] that we need for converting to the sap model.
///
/// ## Representation
/// - Unions should be flattened:
///   - Union members cannot be unions
///   - Union members cannot be optional
///   - Union members cannot be type aliases which themselves resolve to unions (or optionals)
///   - Same rules for the inner type of an optional type

View on GitHub (pinned to bd85ce9dee)

Solutions

  1. Ensure types pass through baml_type::simplify_sap::simplify / normalize_parse_target before conversion so nested unions are flattened.
  2. Rewrite the type to a single flat union, e.g. (string | int) | Foo becomes string | int | Foo.
  3. Avoid aliases that resolve to unions inside other unions; inline the union members directly.

Example fix

// before
type StringsOrInts = string | int
function F() -> StringsOrInts | bool // nested union
// after
function F() -> string | int | bool // flattened
Defensive patterns

Strategy: validation

Validate before calling

// A union member must not be a union or an alias resolving to one
fn union_members_flat(ty: &SapTy, defs: &HashMap<DefKey, SapTy>) -> bool {
    match ty {
        SapTy::Union(items, _) => items.iter().all(|i| !matches!(i, SapTy::Union(..))),
        SapTy::TypeAlias(name, _) => !matches!(defs.get(name), Some(SapTy::Union(..))),
        _ => true,
    }
}

Try / catch

match result {
    Err(ConvertError::UnflattenedUnion) => eprintln!("flatten nested unions before conversion"),
    other => other,
}

Prevention

When it happens

Trigger: TypeCtx::build_db converting a union whose items include a nested SapTy::Union or an alias whose definition is union-like; notably normalize_parse_target can create fresh nested unions like (string | int) | ToolCalls at runtime after generic substitution.

Common situations: Runtime-materialized parse targets after generic substitution bypassing TypeCtx::new's simplification; hand-constructed types that skip the simplify step; aliases aliasing other union aliases.

Understand the failure class

Background: "is not a compatible type" / "cannot merge" errors: when a value's type doesn't match what the library requires — this error's family across 65 libraries.

Related errors


AI-assisted analysis of BoundaryML/baml@bd85ce9dee (2026-09-12). Data as JSON: /api/errors/a3fc0914054449f9. Report an issue: GitHub.