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.