Schema

Schemas define the expected structure of Styx documents for validation purposes. They are optional — deserialization works with target types directly (e.g., Rust structs). Schemas are useful for text editors, CLI tools, and documentation.

Why Styx works for schemas

Styx schemas are themselves Styx documents. This works because of tags and implicit unit:

  • A tag like @string is shorthand for @string@ — a tag with unit payload
  • In schema context, tags name types: @string, @int, @MyCustomType
  • Built-in tags like @union, @map, @enum take payloads describing composite types
  • User-defined type names are just tags referencing definitions elsewhere in the schema

For example:

styx
host @string           // field "host" must match type @string
port @int              // field "port" must match type @int
id @union(@int @string) // @union tag with sequence payload

The @union(@int @string) is:

  • Tag @union with payload (@int @string)
  • The payload is a sequence of two tagged unit values
  • Semantically: "id must match @int or @string"

This uniformity means schemas require no special syntax — just Styx with semantic interpretation of tags as types.

In schema definitions, the unit value @ (not a tag) is used as a wildcard meaning "any type reference" — that is, any tagged unit value like @string or @MyType.

Schema file structure

schema.file

A schema file has three top-level keys: meta (required), imports (optional), and schema (required).

styx
meta {
  id https://example.com/schemas/server
  version 2026-01-11
  description "Server configuration schema"
}

schema {
  @ @object{
    server @Server
  }

  Server @object{
    host @string
    port @int{min 1, max 65535}
  }
}
schema.meta.version

The version field in schema metadata MUST use datever format: YYYY-MM-DD (ISO 8601 date). This enables simple chronological ordering of schema versions.

styx
meta {
  id https://example.com/schemas/config
  version 2026-01-16
}
schema.root

Inside schema, the key @ defines the expected structure of the document root. Other keys define named types that can be referenced with @TypeName.

Imports

schema.imports

The imports block maps namespace prefixes to external schema locations (URLs or paths). Paths are resolved relative to the importing schema file. Imported types are referenced as @namespace.TypeName.

styx
meta {
  id https://example.com/schemas/app
  version 2026-01-11
}

imports {
  common https://example.com/schemas/common.styx
  auth https://example.com/schemas/auth.styx
}

schema {
  @ @object{
    user @auth.User
    settings @common.Settings
  }
}

Schema declaration in documents

schema.declaration

A document MAY declare its schema using the @schema tag at the document root. The value is either a URL/path string (external reference) or an inline schema object. Inline schemas use a simplified form: only the schema block is required; meta and imports are optional.

styx
// External schema reference
@schema https://example.com/schemas/server.styx

server {host localhost, port 8080}
styx
// Inline schema (simplified form)
@schema {
  schema {
    @ @object{server @object{host @string, port @int}}
  }
}

server {host localhost, port 8080}

Types and constraints

schema.type

A tagged unit denotes a type constraint.

styx
version @int     // type: must be an integer
host @string     // type: must be a string

Since unit payloads are implicit, @int is shorthand for @int@ — which makes Styx schemas valid Styx.

schema.literal

A scalar denotes a literal value constraint. The unit value @ is also a literal constraint.

styx
version 1        // literal: must be exactly "1"
enabled true     // literal: must be exactly "true"
tag "@mention"   // literal: must be exactly "@mention" (quoted)
nothing @        // literal: must be exactly @ (unit)

Standard types

schema.type.primitives

These tags are built-in type constraints:

TypeDescription
@stringany scalar
@booltrue or false
@intany integer
@floatany finite floating point number (JSON number syntax)
@unitthe unit value @
@anyany value

Composite type constructors (@optional, @union, @seq, @tuple, @map, @enum, @one-of, @flatten) are described in their own sections. Modifiers (@default, @deprecated) are described in their own sections.

Type constraints

schema.constraints

Scalar types can have constraints specified in an object payload.

schema.constraints.string

@string accepts optional constraints:

ConstraintDescription
minLenminimum length (inclusive)
maxLenmaximum length (inclusive)
patternECMAScript regular expression the string must match

r[schema.constraints.string.pattern] The pattern constraint uses ECMAScript (JavaScript) regular expression syntax as defined in ECMA-262. The pattern is implicitly anchored — it must match the entire string, not just a substring. Implementations SHOULD support at minimum the common subset: character classes, quantifiers, alternation, grouping, and Unicode escapes.

styx
name @string{minLen 1, maxLen 100}
slug @string{pattern "^[a-z0-9-]+$"}
schema.constraints.int

@int accepts optional constraints:

ConstraintDescription
minminimum value (inclusive)
maxmaximum value (inclusive)
styx
port @int{min 1, max 65535}
age @int{min 0}
schema.constraints.float

@float accepts optional constraints:

ConstraintDescription
minminimum value (inclusive)
maxmaximum value (inclusive)
styx
ratio @float{min 0.0, max 1.0}
temperature @float{min -273.15}

r[schema.constraints.float.syntax] Float values use JSON number syntax: an optional minus sign, integer digits, optional decimal fraction, and optional exponent. NaN, Infinity, and -Infinity are NOT valid float values.

float = "-"? integer ("." digits)? exponent?
integer = "0" | [1-9] digits
digits = [0-9]*
exponent = ("e" | "E") ("+" | "-")? digits

Examples: 3.14, -273.15, 6.022e23, 1E-10, 0.0

Optional fields

schema.optional

@optional(@T) matches either a value of type @T or absence of a value. Absence means the field key is not present in the object (it does not mean the field value is @).

styx
server @object{
  host @string
  timeout @optional(@duration)
}

Default values

schema.default

@default(value @T) specifies a default value for optional fields. If the field is absent, validation treats it as if the default value were present. The first element is the default value, the second is the type constraint.

styx
server @object{
  host @string
  port @default(8080 @int{min 1, max 65535})
  timeout @default(30s @duration)
  enabled @default(true @bool)
}

Note: @default implies the field is optional. Using @optional(@default(...)) is redundant.

r[schema.default.validation] The default value MUST satisfy all validation rules of its type constraint. A schema with an invalid default value (e.g., @default(0 @int{min 1})) is itself invalid.

Deprecation

schema.deprecated

@deprecated("reason" @T) marks a field as deprecated. Validation produces a warning (not an error) when deprecated fields are used. The first element is the deprecation message, the second is the type constraint.

styx
server @object{
  host @string
  // Old field, use 'host' instead
  hostname @deprecated("use 'host' instead" @string)
}

Composite types

Objects

schema.object

@object{...} defines an object schema mapping field names (scalars) to schemas. By default, object schemas are closed: keys not mentioned in the schema are forbidden.

To allow additional keys, use a special entry with key @ (unit key) to define the schema for all additional fields. If present, any key not explicitly listed MUST match the @ entry's schema. The key @ is reserved for this purpose and cannot be used to describe a literal unit-key field.

styx
// Closed object (default): only host and port allowed
Server @object{
  host @string
  port @int
}

// Open object: allow any extra string fields
Labels @object{
  @ @string
}

// Mixed: known fields plus additional string→string
Config @object{
  name @string
  @ @string
}

Unions

schema.union

@union(...) matches if the value matches any of the listed types.

styx
id @union(@int @string)           // integer or string
value @union(@string @unit)       // nullable string

Sequences

schema.sequence

@seq(@T) defines a sequence schema where every element matches type @T.

styx
hosts @seq(@string)               // sequence of strings
servers @seq(@object{             // sequence of objects
  host @string
  port @int
})
ids @seq(@union(@int @string))    // sequence of ids

Tuples

schema.tuple

@tuple(@A @B @C ...) defines a fixed-length sequence where each position has a distinct type. Unlike @seq which is homogeneous (all elements same type), @tuple is heterogeneous.

styx
point @tuple(@int @int)           // (x, y) coordinates
entry @tuple(@string @int @bool)  // (name, count, enabled)
range @tuple(@float @float)       // (min, max)

r[schema.tuple.validation] Validation checks that:

  • The value is a sequence
  • The sequence has exactly the expected number of elements
  • Each element matches its corresponding positional type

Maps

schema.map

@map(@K @V) matches an object where all keys match @K and all values match @V. @map(@V) is shorthand for @map(@string @V).

styx
env @map(@string)              // string → string
ports @map(@int)               // string → int

r[schema.map.keys] Valid key types are scalar types that can be parsed from the key's text representation: @string, @int, and @bool. Non-scalar key types (objects, sequences) are not allowed. Key uniqueness is determined by the parsed key value per r[entry.key-equality] in the parser spec, not by the typed interpretation — "1" and "01" are distinct keys even if both parse as integer 1.

Named types

schema.type.definition

Named types are defined inside the schema block. Use @TypeName to reference them. By convention, named types use PascalCase (e.g., TlsConfig, UserProfile). This is not enforced but aids readability and distinguishes user types from built-in types.

styx
TlsConfig @object{
  cert @string
  key @string
}

server @object{
  tls @TlsConfig
}

Recursive types

schema.type.recursive

Recursive types are allowed. A type may reference itself directly or indirectly.

styx
Node @object{
  value @string
  children @seq(@Node)
}

Flatten

schema.flatten

@flatten(@Type) inlines fields from another type into the current object. The document is flat; deserialization reconstructs the nested structure.

styx
User @object{name @string, email @string}

Admin @object{
  user @flatten(@User)
  permissions @seq(@string)
}

Document: name Alice, email alice@example.com, permissions (read write)

r[schema.flatten.constraints] The argument to @flatten MUST be a named object type or a type alias to an object. Flattening unions or primitives is not allowed. If flattened fields conflict with explicitly declared fields in the same object, validation MUST fail. Multiple @flatten entries are allowed; their fields MUST NOT overlap with each other or with explicit fields.

Enums

schema.enum

@enum{...} defines valid variant names and their payloads. Unlike other composite types that use sequence payloads (@union(...), @map(...)), @enum uses an object payload because variants have names.

styx
status @enum{
  ok
  pending
  err @object{message @string}
}

Values use the tag syntax: @ok, @pending, @err{message "timeout"}.

Value constraints (one-of)

schema.one-of

@one-of(@type (value1 value2 ...)) constrains values to a finite set. The first element is the base type, the remaining elements are allowed values.

styx
level @one-of(@string (debug info warn error))
env @one-of(@string (development staging production))
method @one-of(@string (GET POST PUT DELETE PATCH))

r[schema.one-of.validation] Validation first checks that the value matches the base type, then checks that the value is one of the allowed values. Error messages include typo suggestions when the value is close to an allowed value.

r[schema.one-of.any-type] While @one-of is most commonly used with @string, it works with any base type:

styx
priority @one-of(@int (1 2 3 4 5))
enabled @one-of(@bool (true))  // must be true, false not allowed

Validation

schema.validation

Schema validation checks that a document conforms to a schema. Validation produces a list of errors and warnings; an empty error list means the document is valid.

r[schema.validation.errors] Validation errors MUST include:

  • The path to the invalid value (e.g., server.port)
  • The expected constraint (e.g., @int{min 1, max 65535})
  • The actual value or its type

Common error conditions:

  • Type mismatch: value doesn't match the expected type (e.g., "abc" for @int)
  • Constraint violation: value doesn't meet constraints (e.g., 0 for @int{min 1})
  • Missing required field: a non-optional field is absent
  • Unknown field: a field not in the schema (for closed objects)
  • Literal mismatch: value doesn't match a literal constraint
  • Union failure: value doesn't match any variant in a union

r[schema.validation.warnings] Validation warnings are non-fatal issues:

  • Deprecated field: a field marked with @deprecated is present

Meta schema

The schema for Styx schema files.

schema.meta.wildcard

In the meta schema, the unit value @ is used as a wildcard meaning "any type reference" — that is, any tagged unit value like @string or @MyType. This is a semantic convention for the meta schema; it leverages the fact that @ (unit) is a valid Styx value, and in schema context represents "match any type tag here".

styx
meta {
  id https://styx.bearcove.eu/schemas/schema
  version 2026-01-16
  description "Schema for Styx schema files"
}

schema {
  /// The root structure of a schema file.
  @ @object{
    /// Schema metadata (required).
    meta @Meta
    /// External schema imports (optional).
    imports @optional(@map(@string @string))
    /// Type definitions: @ for document root, strings for named types.
    schema @map(@union(@string @unit) @Schema)
  }

  /// Schema metadata.
  Meta @object{
    /// Unique identifier for the schema (URL recommended).
    id @string
    /// Schema version (datever format: YYYY-MM-DD).
    version @string{pattern "^\\d{4}-\\d{2}-\\d{2}$"}
    /// Human-readable description.
    description @optional(@string)
  }

  /// String type constraints.
  StringConstraints @object{
    minLen @optional(@int{min 0})
    maxLen @optional(@int{min 0})
    pattern @optional(@string)
  }

  /// Integer type constraints.
  IntConstraints @object{
    min @optional(@int)
    max @optional(@int)
  }

  /// Float type constraints.
  FloatConstraints @object{
    min @optional(@float)
    max @optional(@float)
  }

  /// A type constraint.
  Schema @enum{
    /// String type with optional constraints.
    string @optional(@StringConstraints)
    /// Integer type with optional constraints.
    int @optional(@IntConstraints)
    /// Float type with optional constraints.
    float @optional(@FloatConstraints)
    /// Boolean type.
    bool
    /// Unit type (the value must be @).
    unit
    /// Any type (accepts any value).
    any
    /// Object schema: @object{field @type, @ @type}.
    object @object{@ @Schema}
    /// Sequence schema: @seq(@type).
    seq(@Schema)
    /// Tuple schema: @tuple(@A @B @C ...).
    tuple @seq(@Schema)
    /// Union: @union(@A @B ...).
    union @seq(@Schema)
    /// Optional: @optional(@T).
    optional(@Schema)
    /// Enum: @enum{variant, variant @object{...}}.
    enum @object{@ @Schema}
    /// Value constraint: @one-of(@type (value1 value2 ...)).
    one-of @seq(@Schema @seq(@any))
    /// Map: @map(@V) or @map(@K @V).
    map @seq(@Schema)
    /// Flatten: @flatten(@Type).
    flatten @
    /// Default value: @default(value @type).
    default @seq(@union(@string @Schema))
    /// Deprecated: @deprecated("reason" @type).
    deprecated @seq(@union(@string @Schema))
    /// Type reference (user-defined type).
    type @
  }
}