dbt-labs/dbt-core · error · minijinja::Error (InvalidOperation)

Expected a list of column definitions

Error message

Expected a list of column definitions

What it means

Thrown by convert_value_to_column_definitions when the value it is iterating is not a list of column definitions. The contract mismatch logic only accepts a sequence whose items are objects with attributes like name/data_type; anything else (e.g. a plain string, dict-of-dicts, or non-sequence) falls into the Err arm of the iterator conversion and aborts.

Source

Thrown at crates/dbt-jinja-utils/src/functions/contract_error.rs:191

                            "Column 'data_type' must be a string",
                        )
                    })?
                    .to_string();

                let formatted = item
                    .get_attr("formatted")
                    .ok()
                    .and_then(|v| v.as_str().map(|s| s.to_string()));

                columns.push(ColumnDefinition {
                    name,
                    data_type,
                    formatted,
                });
            }
        }
        Err(_) => {
            return Err(Error::new(
                ErrorKind::InvalidOperation,
                "Expected a list of column definitions",
            ));
        }
    }

    Ok(columns)
}

#[cfg(test)]
mod tests {
    use super::*;
    use dbt_agate::MappedSequence;

    #[test]
    fn test_contract_error_perfect_match() {
        let yaml_columns = vec![
            ColumnDefinition {

View on GitHub (pinned to 0267ce9170)

Solutions

  1. Make sure the columns value passed to contract checking is a list (Jinja array) of column objects each with name and data_type attributes.
  2. If your columns are a mapping, convert to a list of objects (e.g. [{'name': k, 'data_type': v} ...]).
  3. Print the value with |tojson before the call to confirm its shape.

Example fix

-- before (Jinja)
{% do contract_check(columns={'id': 'int', 'name': 'text'}) %}
-- after
{% do contract_check(columns=[{'name': 'id', 'data_type': 'int'}, {'name': 'name', 'data_type': 'text'}]) %}
Defensive patterns

Strategy: validation

Validate before calling

-- Jinja, before contract check
{% if columns is not sequence or columns is string or columns is mapping %}
  {{ exceptions.raise_compiler_error("columns must be a list of column definitions, got: " ~ columns | tojson) }}
{% endif %}

Try / catch

match convert_value_to_column_definitions(value) {
    Err(e) if e.message().contains("Expected a list of column definitions") => {
        // dump value with |tojson, fix shape to a list of objects
    }
    other => other?,
}

Prevention

When it happens

Trigger: get_contract_mismatches receives a 'columns' value that is not a list (e.g. a mapping, a single object, or a string), or list items are not objects with column attributes, causing the minijinja iteration/attribute extraction to fail.

Common situations: Passing a dict keyed by column name instead of a list of column dicts; macros returning a single column instead of a list; whitespace/formatting changes in rendered macro output producing malformed results; version drift between custom contract macros and the engine.

Understand the failure class

Background: Schema validation failed / invalid input schema: payload rejected because its shape doesn't match the expected schema — this error's family across 28 libraries.

Related errors


AI-assisted analysis of dbt-labs/dbt-core@0267ce9170 (2026-09-07). Data as JSON: /api/errors/81cb7b05fdbc9c49. Report an issue: GitHub.