Skip to content

Config schema

config.schema in matrix.json points to a JSON Schema describing the shape of valid configuration. The SDK does not enforce it — resolveConfiguredPackageConfig does no validation — but a number of downstream tools do. This page covers what to put in the schema, what reads it, and where validation actually happens.

Declaration

json
"config": {
  "schema": "./config/config.schema.json",
  "defaults": "./config/config.defaults.json",
  ...
}

The path is relative to the package root. The file should be a valid JSON Schema document. Convention is ./config/config.schema.json; the actual filename is your choice as long as matrix.json points at it.

What goes in the schema

A typical schema covers:

  • Required vs optional fields. Mark required fields explicitly so missing values fail loudly.
  • Type constraints. string vs integer vs boolean, plus enums for fixed value sets (log levels, providers, modes).
  • Numeric ranges. minimum, maximum for limits.
  • String patterns. Regex constraints for IDs, URLs, etc.
  • Object structure. Nested objects with their own required/optional fields.
  • Default values. JSON Schema default annotations — informational, do not affect resolution; the config.defaults JSON is the one source of truth for runtime defaults.

A schema for a simplified chat package might look like:

json
{
  "$schema": "http://json-schema.org/draft-07/schema#",
  "title": "Chat package configuration",
  "type": "object",
  "additionalProperties": false,
  "required": ["logLevel"],
  "properties": {
    "logLevel": {
      "type": "string",
      "enum": ["debug", "info", "warn", "error"],
      "description": "Minimum log level emitted by the chat runtime."
    },
    "auth": {
      "type": "object",
      "properties": {
        "provider": {
          "type": "string",
          "enum": ["anthropic", "google", "github"],
          "description": "Default identity provider."
        }
      }
    },
    "limits": {
      "type": "object",
      "properties": {
        "maxMessages": {
          "type": "integer",
          "minimum": 1,
          "maximum": 10000,
          "description": "Maximum messages retained per conversation."
        },
        "timeoutMs": {
          "type": "integer",
          "minimum": 100,
          "description": "Request timeout in milliseconds."
        }
      }
    }
  }
}

Keep field descriptions — they are read by matrix config explain and surfaced in tooling.

What does NOT validate the schema

resolveConfiguredPackageConfig does not call any schema validator. It deep-merges layers and returns the result. If you supply env var MATRIX_FOO_LOG_LEVEL=banana, the resolver does not catch the typo — your runtime sees logLevel: 'banana' and either crashes when it tries to use it or silently misbehaves.

This is intentional: the resolver is small, dependency-free, and runs identically in tests and production. Validation is a separate concern.

What DOES validate

The schema is read and used by:

ConsumerWhat it does with the schema
matrix config explain <package>Reads the schema, validates the resolved config, and prints which key came from which layer plus any validation errors.
hivecast doctor --repairReads the schema for each installed package and flags configs that fail to validate.
Editor toolingThe JSON Schema can be referenced by $schema in your config.defaults.json so editors give autocomplete and inline validation.
Your own runtime (recommended)Validate inside onBootstrap() before using the config. Use a small JSON Schema validator (ajv, etc.) and fail fast.

The pattern for actor-side validation:

ts
import Ajv from 'ajv';
import schema from '../../config/config.schema.json';

class MyPackageRoot extends MatrixActor {
  protected async onBootstrap() {
    const config = await loadConfig();
    const validate = new Ajv().compile(schema);
    if (!validate(config)) {
      throw new Error(`Invalid config: ${JSON.stringify(validate.errors)}`);
    }
    this.config = config;
  }
}

Schema vs defaults

These are two different files with overlapping concerns:

  • config.schema.json describes what valid configuration looks like.
  • config.defaults.json is one specific valid configuration.

The defaults must validate against the schema. If they don't, your package is broken on a fresh install. CI typically runs both files through a validator as a smoke test.

Schema and additionalProperties

Set additionalProperties: false on the root object and (recommended) on every nested object. Without it, a typo in an env var produces a new top-level key that silently slips into the config. With it, the validator flags the unexpected key.

json
{ "type": "object", "additionalProperties": false, "properties": { ... } }

When to skip the schema

Small packages with no configuration at all skip both config.schema and config.defaults. The config block is optional. If you don't need tunables, leave it out.

A package with a single tunable (e.g., a debug flag) can still benefit from a one-property schema — it documents the tunable for users who go looking.

See also

Source: projects/matrix-3/packages/core/src/runtime/PackageConfigResolver.ts:20-25 (IPackageConfigContract type), and the config block in real packages like projects/matrix-3/packages/chat/matrix.json:135-156.