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
- Search your codebase for writes to the `_metadata` extension key and ensure only objects (Map) are stored there
- Before applying metadata, coerce or reset `_metadata` to an empty object if it is not already a map
- 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
- Never write non-object values under the reserved `_metadata` extension key
- Use namespaced keys for custom extensions
- When merging schemas, normalize `_metadata` to an object first
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
- already found inputs
- No usable integer type should be unrepresentable by both…
- registered log schema required
- source output misconfigured
- source output misconfigured - output for port
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)