BoundaryML/baml · critical

decodeUnionValue: valueUnion is nil

Error message

decodeUnionValue: valueUnion is nil

What it means

decodeUnionValue panics when handed a nil *cffi.CFFIValueUnionVariant. The decoder requires a non-nil union variant to determine the value's type; a nil pointer is an internal invariant violation in the FFI value decoding path reached through Decode.

Source

Thrown at engine/language_client_go/baml_go/serde/decode.go:226

func decodeEnumValue(valueEnum *cffi.CFFIValueEnum, typeMap TypeMap) (reflect.Value, reflect.Type) {
	if valueEnum == nil {
		panic("decodeEnumValue: valueEnum is nil")
	}

	goType, ok := typeMap.GetType(valueEnum.Name)
	if !ok {
		dynamicEnum := DynamicEnum{Name: valueEnum.Name.Name, Value: valueEnum.Value}
		return reflect.ValueOf(dynamicEnum), reflect.TypeOf(DynamicEnum{})
	}
	enum := reflect.New(goType)
	as_interface := enum.Interface().(BamlEnumDeserializer)
	as_interface.Decode(valueEnum, typeMap)
	return enum.Elem(), goType
}

func decodeUnionValue(valueUnion *cffi.CFFIValueUnionVariant, typeMap TypeMap) (reflect.Value, reflect.Type) {
	if valueUnion == nil {
		panic("decodeUnionValue: valueUnion is nil")
	}

	value, goType := func() (reflect.Value, reflect.Type) {
		if ok := valueUnion.Value.GetNullValue(); ok != nil {
			// If the union value is null, return nil
			return reflect.ValueOf(nil), nil
		} else if valueUnion.IsSinglePattern {
			// For optional patterns (T | null), decode the inner value directly
			// These shouldn't be looked up as union types
			// Ignore the union-ness of it and just decode the inner value
			return Decode(valueUnion.Value, typeMap)
		} else {
			goType, ok := typeMap.GetType(valueUnion.Name)
			if !ok {
				// Union not found
				// This is a fully dynamic union, so we
				// decode the value as the value and drop
				// union type information

View on GitHub (pinned to bd85ce9dee)

Solutions

  1. Check for a nil value before calling Decode and treat it as an empty/null result
  2. Regenerate baml_client and align the BAML runtime version with the Go bindings
  3. Catch the panic at the boundary with recover() if you must survive malformed data
  4. File a bug with BAML including the BAML schema and payload that triggered it

Example fix

// before
val, typ := serde.Decode(value, typeMap)
// after
if value == nil || value.Value == nil {
  return reflect.Value{}, nil
}
val, typ := serde.Decode(value, typeMap)
Defensive patterns

Strategy: type-guard

Validate before calling

if value == nil || value.Value == nil { return reflect.Value{}, nil }

Type guard

func decodable(v *cffi.CFFIValue) bool { return v != nil && v.Value != nil }

Try / catch

func safeDecode(v *cffi.CFFIValue, tm serde.TypeMap) (out reflect.Value) {
  defer func() { if r := recover(); r != nil { out = reflect.Value{} } }()
  v2, _ := serde.Decode(v, tm)
  return v2
}

Prevention

When it happens

Trigger: Decode() on a value whose CFFI representation has a nil value union — e.g. a malformed or nil CFFIValue inside a decoded response.

Common situations: Runtime/generator version mismatch producing values the Go serde cannot interpret, corrupted native responses, or a bug in BAML's value encoding.

Related errors


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