KDL F# is a from-scratch, implementation of the KDL 2.0 document language written in pure F#.
It ships:
KDLFSharp.Core- reusable lexer/parser/AST utilities with strong typing for strings, numbers, booleans, nulls, type annotations, and child blocks.KDLFSharp.CLI- a colored cli app that tokenizes + parses.kdlfiles, then prints the AST as a tree (with node/prop counts, typed values, and severity-styled diagnostics on errors).
The CLI exposes the lexer and parser from the library, rendering KDL documents as colored AST trees with node/property counts, typed values, and severity-styled diagnostics for errors.
View a KDL document as an AST tree
dotnet run --project KDLFSharp.CLI/KDLFSharp.CLI.fsproj data/zellij.kdlParse KDL files and display the AST structure:
# Parse from file
dotnet run --project KDLFSharp.CLI/KDLFSharp.CLI.fsproj data/sample-park.kdl
# Parse from stdin
cat data/sample-park.kdl | dotnet run --project KDLFSharp.CLI/KDLFSharp.CLI.fsproj -- -Convert KDL documents to JSON
# Structured output (default) - AST with explicit types
dotnet run --project KDLFSharp.CLI/KDLFSharp.CLI.fsproj -- kdl-to-json data/sample-park.kdl
# Save to file
dotnet run --project KDLFSharp.CLI/KDLFSharp.CLI.fsproj -- kdl-to-json data/sample-park.kdl --out output.json
# Debug mode - canonical IR for lossless round-trips
dotnet run --project KDLFSharp.CLI/KDLFSharp.CLI.fsproj -- kdl-to-json data/sample-park.kdl --debug
# Sample mode - human-friendly format (lossy)
dotnet run --project KDLFSharp.CLI/KDLFSharp.CLI.fsproj -- kdl-to-json data/sample-park.kdl --sample --lossyConvert JSON back to KDL
# From structured JSON
dotnet run --project KDLFSharp.CLI/KDLFSharp.CLI.fsproj -- json-to-kdl data/sample-park.json
# From debug IR
dotnet run --project KDLFSharp.CLI/KDLFSharp.CLI.fsproj -- json-to-kdl data/sample-park.ir.json --debug
# From sample format
dotnet run --project KDLFSharp.CLI/KDLFSharp.CLI.fsproj -- json-to-kdl data/sample-park.sample.json --sample
# With colored output
dotnet run --project KDLFSharp.CLI/KDLFSharp.CLI.fsproj -- json-to-kdl data/sample-park.json --prettyConvert KDL documents to XML
# Structured output (default)
dotnet run --project KDLFSharp.CLI/KDLFSharp.CLI.fsproj -- kdl-to-xml data/sample-library.kdl
# Debug mode
dotnet run --project KDLFSharp.CLI/KDLFSharp.CLI.fsproj -- kdl-to-xml data/sample-library.kdl --debug
# Sample mode with custom namespace
dotnet run --project KDLFSharp.CLI/KDLFSharp.CLI.fsproj -- kdl-to-xml data/sample-library.kdl --sample --lossy --sample-ns https://example.com/kdl-metaConvert XML back to KDL
# From structured XML
dotnet run --project KDLFSharp.CLI/KDLFSharp.CLI.fsproj -- xml-to-kdl data/sample-library.xml
# From debug IR
dotnet run --project KDLFSharp.CLI/KDLFSharp.CLI.fsproj -- xml-to-kdl data/sample-library.ir.xml --debug
# From sample format
dotnet run --project KDLFSharp.CLI/KDLFSharp.CLI.fsproj -- xml-to-kdl data/sample-library.sample.xml --sampleConversion Output Modes
AST representation with explicit node types, value kinds, and type annotations. Suitable for programmatic access and preserves the full KDL structure.
Canonical intermediate representation (IR) defined in KDLFSharp.Core.JSON and KDLFSharp.Core.XML.
Enables lossless round-trip conversions with complete fidelity.
Human-friendly format where properties and children are surfaced as plain fields with metadata in _meta blocks.
Lossy by design and optimized for readability over round-trip accuracy. Requires explicit --lossy acknowledgement.
# See help for an exhaustive list of options
dotnet run --project KDLFSharp.CLI/KDLFSharp.CLI.fsproj -- helpPrereqs: .NET 9 SDK
# Format/build everything
dotnet build
# Run Tests
dotnet run --project KDLFSharp.Tests/KDLFSharp.Tests.fsproj
# View AST
dotnet run --project KDLFSharp.CLI/KDLFSharp.CLI.fsproj data/sample-park.kdl
dotnet run --project KDLFSharp.CLI/KDLFSharp.CLI.fsproj data/zellij.kdlThe KDLFSharp.Core.Serialize and KDLFSharp.Core.Deserialize modules provide automatic conversion between F# records and KDL documents using reflection.
open KDLFSharp.Core
open KDLFSharp.Core.Serialize
open KDLFSharp.Core.Deserialize
// Define a record type
type Person = {
Name: string
Age: int
Height: float
IsActive: bool
}
// Create an instance
let person = {
Name = "Alice"
Age = 30
Height = 5.6
IsActive = true
}
// Serialize to KDL string
match Serialize.toString SerializeConfig.Default person with
| Ok kdl -> printfn "%s" kdl
| Error e -> printfn "Error: %O" e
// Deserialize from KDL string
let kdlText = """
Person {
Name="Bob"
Age=25
Height=6.1
IsActive=false
}
"""
match Deserialize.fromString<Person> SerializeConfig.Default kdlText with
| Ok person -> printfn "Loaded: %s, age %d" person.Name person.Age
| Error e -> printfn "Error: %O" eThe serializer supports:
- Primitives:
string,bool,int,int64,float,float32,decimal,byte,int16,uint16,uint32,uint64 - Special values:
infinity,negative infinity,NaN - Optional fields:
option<'T>(None values are omitted) - Lists and arrays:
'T list,'T array - Nested records: Serialized as child nodes
- Null values: Represented as
#null
Optional Fields
type Contact = {
Name: string
Email: string option
Phone: string option
}
let contact = {
Name = "Charlie"
Email = Some "charlie@example.com"
Phone = None // Will be omitted from KDL
}Nested Records
type Address = {
Street: string
City: string
ZipCode: string
}
type PersonWithAddress = {
Name: string
Age: int
Address: Address // Serialized as child node
}
let person = {
Name = "David"
Age = 28
Address = {
Street = "123 Main St"
City = "Austin"
ZipCode = "78701"
}
}Results in KDL:
PersonWithAddress {
Name="David"
Age=28
Address {
Street="123 Main St"
City="Austin"
ZipCode="78701"
}
}Lists and Arrays
type Team = {
Name: string
Members: string list
Scores: int array
}
let team = {
Name = "Engineering"
Members = ["Alice"; "Bob"; "Charlie"]
Scores = [| 95; 87; 92 |]
}Lists of primitives become arguments, while lists of records become child nodes:
type Company = {
Name: string
Employees: Person list
}Customize serialization behavior with `SerializeConfig`
let config = {
NestedRecordsAsChildren = true // Nest records as child nodes
ListItemsAsChildren = false // Lists as arguments (default)
IncludeTypeAnnotations = true // Add type info for round-trips
RootNodeName = Some "custom-name" // Override node name
}
toString config myRecordAll serialization operations return `Result<'T, SerializeError>`
type SerializeError =
| UnsupportedType of string
| MissingRequiredField of string
| TypeMismatch of expected: string * actual: string
| InvalidValue of string
| DeserializationFailed of stringSerialization (Serialize module):
toNode: SerializeConfig -> obj -> SerializeResult<Node>- Serialize record to NodetoDocument: SerializeConfig -> obj -> SerializeResult<Document>- Serialize to DocumenttoString: SerializeConfig -> obj -> SerializeResult<string>- Serialize to KDL string
Deserialization (Deserialize module):
fromNode<'T>: SerializeConfig -> Node -> SerializeResult<'T>- Deserialize Node to recordfromDocument<'T>: SerializeConfig -> Document -> SerializeResult<'T>- Deserialize from DocumentfromString<'T>: SerializeConfig -> string -> SerializeResult<'T>- Deserialize from KDL string
- JSON <-> KDL
- XML <-> KDL
- Record serialization to Map records to KDL and back
- Parse and manipulate KDL documents programmatically (Document Model)
- Query Language
- Schema Validation

