Config Schema Language (CSL) is a language that describes the schema of a configuration. It combines readability with formal structure. The syntax takes inspiration from TypeScript, C++ and Rust, using familiar braces and type annotations while adding declarative constraints.
// Root configuration structure
config MyAppConfig {
// Basic key-value pairs (mandatory by default)
app_name: string;
version: string;
environment: "dev" | "staging" | "prod" = "dev"; // Optional default value
// Optional key (denoted by '?')
timeout?: number @min(0) @max(60); // Annotations for validation
// Nested table
database: {
host: string;
port: number @range(1024, 65535);
credentials?: { // Optional sub-table
username: string;
password: string;
};
};
// Array of objects
endpoints: {
path: string;
method: "GET" | "POST" | "PUT";
rate_limit?: number;
}[];
// Unspecified table/array
metadata?: any{};
debug_flags?: any[];
// Nested table with optional unspecified content
services: {
name: string;
config?: any{}; // Any subtable allowed
}[];
// Array of unspecified tables
raw_data?: any{}[];
// Key relationships & constraints
constraints {
// Conflicts: 'ssl' cannot coexist with 'insecure_mode'
conflicts database.ssl with insecure_mode;
// Dependency: If 'credentials' exists, 'environment' must be "prod"
requires database.credentials => environment == "prod";
// Custom validation: If 'environment' is "prod", 'timeout' must be > 10
validate environment == "prod" ? timeout > 10 : true;
// Conflicts: "debug_flags" cannot coexist with "production_mode"
conflicts debug_flags with production_mode;
// Dependency: If "metadata" exists, "version" must exist (no value check)
requires metadata => version;
// Mixed dependency: If "services" exists, "app_name" must match a regex
requires services => app_name @regex("^svc-.*");
};
}
Token Format
Comments:
//
marks the rest of the line as a comment, except when inside a string.
Numbers: Decimal numbers (e.g.,
123
, 3.14
and 1e5
),
binary numbers (start with 0b
), octal numbers
(start with 0o
), hexadecimal numbers (start with
0x
) and special numbers (nan
,
+nan
, -nan
, inf
,
+inf
and -inf
). Use a
_
to enhance readability (e.g.,
1_000_000
).
Booleans: true
/false
.
Strings: Normal string literals
("..."
, where escape sequences are
allowed) and raw string literals
(R"delim(...)delim"
, where the delimiter
matches
[a-zA-Z0-9!\"#%&'*+,\-.\/:;<=>?\[\]^_{|}~]{0,16}
). The valid escape sequences in normal strings are:
Escape Sequence | Notes |
---|---|
\a |
\x07 alert (bell) |
\b |
\x08 backspace |
\t |
\x09 horizonal tab |
\n |
\x0A newline (or line feed) |
\v |
\x0B vertical tab |
\f |
\x0C form feed |
\r |
\x0D carriage return |
\" |
quotation mark |
\' |
apostrophe |
\? |
question mark (used to avoid trigraphs) |
\\ |
backslash |
\` |
backtick |
\ + up to 3 octal digits |
|
\x + any number of hex digits |
|
\u + 4 hex digits (Unicode BMP) |
|
\U + 8 hex digits (Unicode astral planes)
|
|
any other invalid sequences |
content without \ (e.g., \c is
interpreted as c )
|
Date-times: ISO 8601 date-times.
Durations: ISO 8601 durations or the following shorthand format:
Unit | Suffix | Example | Notes |
---|---|---|---|
Year | y |
1y |
|
Month | mo |
6mo |
Avoids conflict with m (minute). |
Week | w |
2w |
|
Day | d |
3d |
|
Hour | h |
4h |
|
Minute | m |
30m |
|
Second | s |
15s |
|
Millisecond | ms |
200ms |
Identifiers (Keys):
[a-zA-Z_][a-zA-Z0-9_]*
or content enclosed in backticks (`
) using the same rules as string literals.
key: number;
`quotedKey1`: number;
R`delim(quotedKey2)delim`: number;
Type System
Primitives: string
,
number
, boolean
,
datetime
, duration
.
Enums:
"GET" | "POST"
(union of
literals).
Tables:
{ key: type; ... }
(nestable objects).
Arrays: type[]
(e.g.,
string[]
, {...}[]
).
Union Type:
Use the | operator to allow multiple types or literals.
port: number | string; // Number or string
debug: boolean | "verbose"; // Boolean or the literal "verbose"
Specifying a type that a literal in the union has is not allowed.
config Example {
// ❌ Invalid: "info" is a string literal
log_level: string | "info";
}
Optionality
key: type;
key?: type;
Annotations (Inline Validation)
@min(10)
, @max(100)
– Numeric
bounds. Applies only to number type.
@range(10, 100)
– Numeric range. Applies only to
number type.
@int
, @float
– Numeric type. Applies
only to number type.
@regex("^[a-z]+$")
– Regex for strings.
Applies only to strings type.
@start_with("/usr/")
– Starts-with
assertion for strings. Applies only to strings type.
@end_with(".csl")
– Ends-with assertion
for strings. Applies only to strings type.
@contain("temp")
– Contains assertion
for strings. Applies only to strings type.
@min_length(10)
, @max_length(100)
,
@length(50)
– String length assertion. Applies
only to strings type.
@format(email)
– Built-in formats. Applies only
to strings type. Available formats:
(?:[a-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\.[a-z0-9!#$%&'*+/=?^_`{|}~-]+)*|"(?:[\x01-\x08\x0b\x0c\x0e-\x1f\x21\x23-\x5b\x5d-\x7f]|\\[\x01-\x09\x0b\x0c\x0e-\x7f])*")@(?:(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?|\[(?:(?:(2(5[0-5]|[0-4][0-9])|1[0-9][0-9]|[1-9]?[0-9]))\.){3}(?:(2(5[0-5]|[0-4][0-9])|1[0-9][0-9]|[1-9]?[0-9])|[a-z0-9-]*[a-z0-9]:(?:[\x01-\x08\x0b\x0c\x0e-\x1f\x21-\x5a\x53-\x7f]|\\[\x01-\x09\x0b\x0c\x0e-\x7f])+)\])
([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12})
(25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9]?[0-9])\.(25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9]?[0-9])\.(25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9]?[0-9])\.(25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9]?[0-9])
(?:[0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4}|(?:[0-9a-fA-F]{1,4}:){1,7}:|(?:[0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|(?:[0-9a-fA-F]{1,4}:){1,5}(?::[0-9a-fA-F]{1,4}){1,2}|(?:[0-9a-fA-F]{1,4}:){1,4}(?::[0-9a-fA-F]{1,4}){1,3}|(?:[0-9a-fA-F]{1,4}:){1,3}(?::[0-9a-fA-F]{1,4}){1,4}|(?:[0-9a-fA-F]{1,4}:){1,2}(?::[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:(?::[0-9a-fA-F]{1,4}){1,6}|:((?::[0-9a-fA-F]{1,4}){1,7}|:)|fe80:(?::[0-9a-fA-F]{0,4}){0,4}%[0-9a-zA-Z]+|::(ffff(:0{1,4}){0,1}:){0,1}(25[0-5]|(2[0-4][0-9]|(1[01][0-9]|[1-9]?[0-9]))\.){3}(25[0-5]|(2[0-4][0-9]|(1[01][0-9]|[1-9]?[0-9])))
(?:(?:https?|ftp):\/\/)?(?:\S+(?::\S*)?@)?((?:(?!-)[A-Za-z0-9-]{0,62}[A-Za-z0-9]\.)+[A-Za-z]{2,6}|(?:\d{1,3}\.){3}\d{1,3})(?::\d{2,5})?(?:\/[^\s?#]*)?(?:\?[^\s#]*)?(?:#[^\s]*)?
\+?[0-9]{1,4}?[-. ]?\(?[0-9]{1,4}?\)?[-. ]?[0-9]{1,4}[-. ]?[0-9]{1,9}
@deprecated("This key is deprecated.")
– Marks a key as deprecated. This annotation always evaluates
to true.
retries: "unlimited" | number @min(0) @deprecated("This key is deprecated.");
Constraints Block
conflicts <key> with <key>;
requires <key> => <condition>;
validate <expression>;
(boolean logic).
Expressions
environment == "prod"
,
database.port > 1024
.
==
, !=
,
<
, >
, <=
,
>=
.
!
, &&
, ||
.
? :
.Unspecified Tables/Arrays
any{}
as the type.
Accepts any table value (treated as a generic key-value table).
any[]
as the type.
Accepts any array (elements can be mixed types).
any{}
/any[]
sacrifices validation for flexibility. Use sparingly.
any{}
usage.
any{}
/any[]
cannot have nested
constraints/annotations. They are "opaque" types.
Dependency Syntax
requires key_A => key_B
: Ensures
existence of key_B
(not value of
key_B
being positive) if key_A
exists
(not value of key_A
being positive).
requires ssl => domain @regex("\\.example\\.com$");
Explicit Existence Checks
exists(key)
in expressions for custom logic:
validate exists(database.credentials) ? environment == "prod" : true;
Wildcard Key (*
):
Indicates "any key name is allowed here."
The value type after :
enforces the structure for
all matched entries in the table.
A wildcard key matches any keys unless they are explicitly defined. Explicit keys take precedence over wildcards.
config BuildConfig {
// All keys under "target" must have values of type `{ lib_path: string; bin_path: string }`
target: {
x86: { lib_path: string; }; // Explicit key
*: { lib_path: string; bin_path: string; }; // Wildcard
};
}
This means:
target
is a table where
any key (e.g., riscv
,
arm
, etc.) is allowed.
target.x86
only requires
lib_path
(wildcard bin_path
is ignored
for this key).
arm
) must be a
table with lib_path
and bin_path
(both
strings).
Wildcard keys works with nested structures. For example:
platforms: {
*: { // Dynamic platform names (e.g., "windows", "linux")
arch: {
*: { lib_path: string; }; // Dynamic arch keys (e.g., "x86", "arm64")
};
};
};
You can enforce additional rules on the dynamic keys or their values
using a constraints
block:
config BuildConfig {
target: {
*: {
lib_path: string @starts_with("/usr/");
bin_path: string;
};
};
constraints {
// Require at least one key in "target"
validate count_keys(target) > 0;
// All keys in "target" must match a regex (e.g., lowercase/numbers)
validate all_keys(target) match "^[a-z0-9_]+$";
};
}
count_keys(table)
– Returns the number of keys in a
table.
all_keys(table)
– Returns the keys of a table for
validation (e.g., regex checks).
wildcard_keys(table)
– Returns the wildcard keys of a
table for validation (e.g., regex checks).
all_keys
and
wildcard_keys
to validate the name of the keys.
subset
function in the
constraints
block:
constraints {
// Basic subset check (all elements of `selected` exist in `allowed`)
validate subset(selected_features, allowed_features);
// Optional: Specify comparison keys for objects (e.g., match by id)
validate subset(selected_plugins, available_plugins, [id]);
}
constraints
).
database.credentials.username
) to reference nested keys.
= value
(e.g.,
environment = "dev"
).
Constraints in a nested block can only reference keys within their immediate scope.
constraints
are defined.
config Server {
database: {
ssl: boolean;
port: number;
constraints {
// ✅ Valid: `ssl` and `port` are in the same block
conflicts ssl with port;
};
};
insecure_mode: boolean;
constraints {
// ✅ Valid: Constraints can reference keys in the same block (`insecure_mode`)
// and nested keys via their **relative path** (`database.ssl`).
conflicts database.ssl with insecure_mode;
};
}
config Example {
log_level: "debug" | "info";
logger: {
format: string;
constraints {
// ❌ Invalid: `log_level` is in the parent scope
conflicts format with log_level;
};
};
}
*
block apply to
all instances of dynamic keys. For example:
target: {
*: {
lib_path: string;
constraints {
// Applies to every key under "target":
// If `lib_path` contains "temp", `bin_path` must exist.
validate lib_path @contains("temp") ? exists(bin_path) : true;
};
};
};
Parent constraints cannot be overridden by child constraints.
Constraints are additive: Parent and child constraints both apply.
Conflicting constraints (e.g., a parent says A
is
required, a child says A
is forbidden) will make
both constraints invalid.
Constraints are never overridden — they are cumulative. For example:
config Example {
constraints {
requires nested.a => b; // Parent rule
};
nested: {
a?: boolean;
constraints {
requires a => c; // Child rule (additive)
};
};
}
If nested.a
exists, both
b
(from root) and c
(from nested) must
exist.
If nested.a
and nested.c
exist but
b
doesn’t, validation fails due to
the root constraint.
subset
Key Rules
Primitive Arrays (strings, numbers, etc.):
properties
parameter. Uses strict equality
("auto" === "auto"
).
subset(selected_ports, allowed_ports)
.
Object Arrays:
properties
to specify which keys to compare
(e.g., [id]
).
Composite Keys:
[region, env]
).
All must match.
Edge Cases:
source_array
is always valid.target_array
+ non-empty
source_array
→ invalid.
Tooling Behavior:
properties
exist in both
source_array
and
target_array
schemas.
properties
are specified for primitive
arrays.
CSL strikes a deliberate balance between flexibility and rigor, making it effective for real-world configuration management:
Human-Centered Design
key: value
structures.
*
) and
any{}
/any[]
enable gradual schema
adoption—teams can start loosely and tighten constraints
incrementally.
Precision Without Verbosity
"dev" | "prod"
) and enums enforce strict allowed values
without bloated boilerplate.
conflicts
, requires
) declaratively model
complex key relationships, replacing fragile ad-hoc validation
scripts.
Context-Aware Validation
database.port
) enable cross-key validation while
nested constraints keep local logic self-contained.
Dynamic Config Support
all_keys()
, regex
validations) enforce naming conventions or key patterns at scale.
Tooling-Ready Structure
subset
validation and
existence checks (exists()
) cover
advanced use cases like feature toggles or plugin dependencies.
Safety by Default
conflicts X with Y
) prevents invalid states that manual reviews often miss.
CSL avoids the extremes of rigid, unmaintainable schemas or overly permissive "anything goes" configurations. It codifies best practices—type safety, proactive validation, and clear relationships—while staying adaptable to evolving requirements.