How KCL Schemas Provide Advanced Type System Capabilities Beyond Traditional Approaches
KCL schemas function as first-class type system primitives that combine object-oriented inheritance, structural typing via protocols, mixins, index signatures, and compile-time validation to create a constraint-based configuration language.
Unlike simple data structures or basic type definitions found in JSONSchema or YAML, the KCL language treats schemas as rich type objects capable of expressing complex configuration contracts. According to the kcl-lang/kcl source code, the SchemaType struct in crates/sema/src/ty/mod.rs implements a sophisticated type system that bridges compile-time static analysis with runtime validation, enabling developers to define reusable, extensible configuration models with built-in validation logic.
Named Types with Package Context
KCL schemas exist as fully qualified named types within their package namespace. The compiler generates qualified names using the @pkg.Schema format, allowing schemas to be referenced unambiguously across module boundaries.
In crates/sema/src/ty/mod.rs, the SchemaType::ty_str_with_at_pkgpath_prefix method (lines 70-77) constructs these fully qualified identifiers. This enables type-safe imports and prevents naming collisions when composing configurations from multiple libraries.
Instance vs. Type Distinction
The type system maintains a strict distinction between a schema as a type and a schema as an instance. The same identifier can denote the type Person or an instantiated value person = Person{}, with the runtime distinguishing between them via the is_instance field in SchemaType (lines 30-47 of crates/sema/src/ty/mod.rs).
This dual nature allows schemas to serve both as blueprints for validation and as factory functions for creating validated configuration objects.
Single Inheritance and Multiple Mixins
KCL supports traditional single inheritance through the base field (Option<Box<SchemaType>> at lines 53-55) while simultaneously enabling multiple mixins via the mixins: Vec<SchemaType> field (lines 58-60). This combination provides the reusability of inheritance with the compositional flexibility of traits.
When the resolver constructs schema types in crates/sema/src/resolver/global.rs, the build_schema_type function collects inherited attributes and validates that mixin compositions do not create conflicting attribute definitions.
Protocols and Structural Typing
Beyond nominal typing, KCL implements structural typing through protocols. A schema can declare a protocol—a set of required attributes—and any schema satisfying those requirements is assignable to the protocol type, regardless of explicit inheritance.
The protocol: Option<Box<SchemaType>> field (lines 56-57) stores these constraints. The is_sub_schema_of function in crates/sema/src/ty/unify.rs (lines 104-112) validates protocol conformance by checking that all required members exist in the target schema, enabling duck-typing for configuration interfaces.
Index Signatures for Dynamic Keys
Schemas support index signatures similar to TypeScript, allowing arbitrary keys with uniform value types. The index_signature: Option<Box<SchemaIndexSignature>> field (lines 63-65) enables schemas to accept dynamic configuration keys while maintaining type safety.
During evaluation, SchemaEvalContext::is_fit_config in crates/evaluator/src/schema.rs validates that only declared attributes or keys matching the index signature appear in the configuration.
Optional Attributes and Default Values
Attributes support optional markers and default expressions through the SchemaAttr fields is_optional, has_default, and default. When the resolver processes schemas in crates/sema/src/resolver/global.rs, it captures these properties to influence both validation logic and instance creation.
Optional attributes with defaults reduce boilerplate while ensuring that configurations remain valid even when users omit common settings.
Decorators and Validation Logic
Both schemas and individual attributes can attach decorators—functions that run custom validation or transformation logic. The decorators: Vec<Decorator> field (lines 65-66) stores these annotations, which execute during the schema_check phase.
This mechanism allows cross-cutting concerns like range validation, regex matching, or custom business rules to be declared declaratively alongside type definitions.
Rules as Validation-Only Types
KCL introduces rules—special schema types that contain only validation logic and cannot be instantiated. Rules share the same SchemaType representation but set is_rule = true (lines 51-53).
Rules enable reusable validation logic that can be applied across multiple schemas without creating inheritance hierarchies, supporting the "mix in validation" pattern for configuration governance.
Runtime Type Information and Lazy Evaluation
At runtime, the SchemaEvalContext struct defined in crates/evaluator/src/schema.rs (lines 30-45) carries AST nodes, lazy-evaluation scopes, configuration metadata, and evaluation state flags. This context prevents repeated work during configuration generation and supports lazy evaluation of schema attributes.
The evaluator uses this runtime type information to enforce constraints that cannot be verified statically, ensuring that generated JSON/YAML output conforms to the schema definitions.
Practical Implementation Examples
Basic Schema with Optional Attributes
schema Person:
name: str
age?: int = 30 # optional, defaults to 30
The resolver creates a SchemaAttr with is_optional = true and default = Some(30), initialized through SchemaEvalContext::init_lazy_scope.
Inheritance and Mixins
schema Base:
id: str
schema MixinA:
a: int
schema MixinB:
b: bool
schema Derived inherits Base mixins MixinA, MixinB:
name: str
Here, Derived receives base = Some(Base) and mixins = [MixinA, MixinB], with the type system merging attributes and validating name uniqueness.
Protocol-Based Structural Typing
schema PrintableProtocol:
print(): str
schema Foo:
name: str
print(): str = "foo"
# Foo is assignable to PrintableProtocol because it implements `print`
The protocol field enables interface-like behavior without explicit inheritance declarations.
Dynamic Configuration with Index Signatures
schema Config:
[key: str]: any # any key maps to any value
config = Config {
"host": "localhost",
"port": 8080,
}
The index_signature stores key_ty = str and val_ty = any, validated at runtime by SchemaEvalContext::is_fit_config.
Validation Rules
rule PositiveNumber:
x: int
check x > 0
schema Product:
price: int
check PositiveNumber(price)
Rules with is_rule = true provide reusable validation blocks that cannot be instantiated as data.
Summary
- First-class type primitives: KCL schemas in
crates/sema/src/ty/mod.rsimplementSchemaTypeas a comprehensive type object supporting inheritance, mixins, and protocols. - Structural and nominal typing: The system combines traditional inheritance (
basefield) with structural protocols (is_sub_schema_ofinunify.rs) for flexible interface definitions. - Runtime validation:
SchemaEvalContextincrates/evaluator/src/schema.rsbridges compile-time types with runtime configuration validation and lazy evaluation. - Advanced features: Index signatures, decorators, optional attributes with defaults, and rules provide capabilities exceeding traditional configuration schemas or simple struct definitions.
Frequently Asked Questions
How does KCL handle schema inheritance conflicts?
When a schema inherits from a base and mixes in multiple schemas, the resolver in crates/sema/src/resolver/global.rs validates that attribute names do not clash during build_schema_type. If conflicting attributes exist across mixins or between the base and mixins, the compiler raises an error at resolution time, ensuring unambiguous attribute resolution.
What is the difference between a protocol and a base schema in KCL?
A base schema establishes an "is-a" relationship through single inheritance (base: Option<Box<SchemaType>>), carrying both attributes and methods. A protocol (protocol field) defines structural requirements—any schema containing the required members is assignable to the protocol type, regardless of inheritance hierarchy, enabling duck-typing for configuration interfaces.
Can KCL schemas validate values at runtime or only at compile time?
KCL performs both static type checking and runtime validation. The compiler validates type assignments and schema conformance using is_sub_schema_of in crates/sema/src/ty/unify.rs. At runtime, SchemaEvalContext in crates/evaluator/src/schema.rs executes validation checks, evaluates default expressions, and ensures that dynamic keys conform to index signatures before generating output.
What are schema decorators and how do they work?
Decorators are metadata annotations stored in decorators: Vec<Decorator> (lines 65-66 of crates/sema/src/ty/mod.rs) that attach to schemas or attributes. During the schema_check phase, these decorators execute custom validation or transformation logic, enabling cross-cutting concerns like range validation (@range(min=0, max=120)) without polluting the core schema definition with imperative validation code.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →