vectordotdev/vector · error

metadata extension must always be a map

Error message

metadata extension must always be a map

What it means

In vector-config's schema helpers, `apply_custom_attributes` reads the `_metadata` extension on a SchemaObject, inserting an empty JSON object if absent, and panics with "metadata extension must always be a map" if the value under `_metadata` is not a JSON object. The invariant holds only if nothing else writes non-object data under that key. A panic here means some code path (or a hand-built schema) stored a non-object value in the `_metadata` extension.

Solutions

  1. Search your codebase for writes to the `_metadata` extension key and ensure only objects (Map) are stored there
  2. Before applying metadata, coerce or reset `_metadata` to an empty object if it is not already a map
  3. Namespace custom extensions under a different key (e.g. `_custom_metadata`) to avoid colliding with vector-config's reserved `_metadata`

Example fix

// before
.extensions.entry("_metadata".to_string())
    .or_insert_with(|| Value::Object(Map::new()))
    .as_object_mut()
    .expect("metadata extension must always be a map");
// after
.extensions.entry("_metadata".to_string())
    .and_modify(|v| if !v.is_object() { *v = Value::Object(Map::new()); })
    .or_insert_with(|| Value::Object(Map::new()))
    .as_object_mut()
    .expect("metadata extension must always be a map");
Defensive patterns

Strategy: validation

Validate before calling

// Validate the extension before applying metadata:
if let Some(v) = schema.extensions.get("_metadata") {
    assert!(v.is_object(), "_metadata must be a JSON object");
}

Type guard

fn metadata_is_map(schema: &SchemaObject) -> bool {
    schema.extensions.get("_metadata").map_or(true, |v| v.is_object())
}

Try / catch

// guard around metadata application:
if metadata_is_map(&schema) {
    apply_configurable_metadata(&mut schema, metadata);
}

Prevention

When it happens

Trigger: Calling `apply_configurable_metadata`/`apply_custom_attributes` on a schema whose `.extensions["_metadata"]` was pre-populated with a non-object Value (string, array, number) by another extension writer or by merging schemas from incompatible sources.

Common situations: Custom code or downstream tooling writing into schema extensions and colliding with vector-config's `_metadata` key; merging generated schemas where one side set `_metadata` to a non-map value.

Understand the failure class

Background: "This is a bug, please report it": internal invariant violations, unreachable panics, and SNH errors explained — this error's family across 47 libraries.

Related errors


AI-assisted analysis of vectordotdev/vector@bdb87aeaa4 (2026-09-16). Data as JSON: /api/errors/003c0759329c1890. Report an issue: GitHub.

Appendix: source

Thrown at lib/vector-config/src/schema/helpers.rs:102

        .default_value()
        .map(ToValue::to_value)
        .or(schema_metadata.default);
    schema_metadata.deprecated = metadata.deprecated();
    schema.metadata = Some(schema_metadata);
}

fn apply_custom_attributes(
    schema: &mut SchemaObject,
    metadata: &Metadata,
    type_name: &'static str,
) {
    let map_entries_len = {
        let custom_map = schema
            .extensions
            .entry("_metadata".to_string())
            .or_insert_with(|| Value::Object(Map::new()))
            .as_object_mut()
            .expect("metadata extension must always be a map");

        if let Some(message) = metadata.deprecated_message() {
            custom_map.insert(
                "deprecated_message".to_string(),
                serde_json::Value::String(message.to_string()),
            );
        }

        for attribute in metadata.custom_attributes() {
            match attribute {
                CustomAttribute::Flag(key) => {
                    match custom_map.insert(key.to_string(), Value::Bool(true)) {
                        // Overriding a flag is fine, because flags are only ever "enabled", so there's
                        // no harm to enabling it... again. Likewise, if there was no existing value,
                        // it's fine.
                        Some(Value::Bool(_)) | None => {}
                        // Any other value being present means we're clashing with a different metadata
                        // attribute, which is not good, so we have to bail out.

View on GitHub (pinned to bdb87aeaa4)