BoundaryML/baml · error

Expected a null value

Error message

Expected a null value

What it means

ValueExpr::resolve_null asserts that an expression resolves to ResolvedValue::Null, used when config explicitly expects a null value. Any other resolved variant (or a resolution failure) produces the opaque 'Expected a null value' error, hiding the real cause.

Solutions

  1. Resolve with resolve() first to check the actual variant instead of asserting null blindly.
  2. If the value should be null, remove or unset the config field / env var so it resolves to Null.
  3. If 'null' comes from a string literal, use JSON null in config rather than the quoted string "null".
  4. Match on ResolvedValue yourself and treat Null as one case rather than erroring.
  5. Add an explicit default of null in the config schema if the field is optional.

Example fix

// before (config sets value: "null" as a string)
expr.resolve_null(&ctx)?; // Err: Expected a null value
// after
// config: value null (unquoted JSON null)
expr.resolve_null(&ctx)?;
Defensive patterns

Strategy: type-guard

Validate before calling

let resolved = expr.resolve(&ctx)?;
if !matches!(resolved, ResolvedValue::Null(_)) {
    bail!("expected null, got: {resolved:?}");
}

Type guard

fn is_null(v: &ResolvedValue) -> bool { matches!(v, ResolvedValue::Null(..)) }

Try / catch

if let Err(e) = expr.resolve_null(&ctx) {
    log::debug!("value present, not null: {e}");
}

Prevention

When it happens

Trigger: Calling resolve_null on an expression that resolves to any concrete value — a string, number, array, or map — e.g. code that treats an optional field as null when the user actually supplied a value.

Common situations: Optional config fields where the developer assumed the default was null but a value was set; env var interpolation that turned 'null' into the string "null" rather than a real null.

Understand the failure class

Background: Type mismatch errors: IllegalArgumentException, TypeError and type guards across 150 open-source libraries — this error's family across 150 libraries.

Related errors


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

Appendix: source

Thrown at engine/baml-lib/baml-types/src/value_expr.rs:576

    pub fn resolve_map(&self, ctx: &impl GetEnvVar) -> Result<IndexMap<String, ResolvedValue>> {
        match self.resolve(ctx) {
            Ok(ResolvedValue::Map(m, ..)) => Ok(m.into_iter().map(|(k, (_, v))| (k, v)).collect()),
            _ => Err(anyhow::anyhow!("Expected a map")),
        }
    }

    pub fn resolve_numeric(&self, ctx: &impl GetEnvVar) -> Result<String> {
        match self.resolve(ctx) {
            Ok(ResolvedValue::Numeric(n, ..)) => Ok(n),
            _ => Err(anyhow::anyhow!("Expected a numeric value")),
        }
    }

    pub fn resolve_null(&self, ctx: &impl GetEnvVar) -> Result<()> {
        match self.resolve(ctx) {
            Ok(ResolvedValue::Null(..)) => Ok(()),
            _ => Err(anyhow::anyhow!("Expected a null value")),
        }
    }

    pub fn resolve_serde<T: serde::de::DeserializeOwned>(&self, ctx: &impl GetEnvVar) -> Result<T> {
        let value = self.resolve(ctx)?;
        let value: serde_json::Value = value.try_into()?;
        match serde_json::from_value(value) {
            Ok(v) => Ok(v),
            Err(e) => Err(anyhow::anyhow!("Failed to deserialize value: {e}")),
        }
    }

    /// Resolve and deserialize, with support for template_string calls.
    pub fn resolve_serde_with_templates<T: serde::de::DeserializeOwned>(
        &self,
        ctx: &impl GetEnvVar,
        template_renderer: &impl TemplateStringRenderer,
    ) -> Result<T> {

View on GitHub (pinned to bd85ce9dee)