NAME
Text::KDL::XS::Cookbook - Every KDL feature and how to use it from Perl
DESCRIPTION
This document walks through the complete KDL Document Language, one feature at a time, and shows for each feature what it looks like in a KDL file, what Text::KDL::XS turns it into, and how to produce it again with "emit_kdl" in Text::KDL::XS. It ends with a set of recipes for common tasks.
The reference documentation for the API lives in Text::KDL::XS. This document assumes you know that parse_kdl returns a Text::KDL::XS::Document holding Text::KDL::XS::Node objects whose arguments and properties are Text::KDL::XS::Value objects.
Conventions used in the examples:
Where output is shown, it was produced by running the code against this version of Text::KDL::XS. Where the code and its output differ from the KDL specification, the text says so.
A Perl snippet that uses
$kdlwithout defining it refers to the KDL text shown immediately above it. Snippets that use$docor$nodecontinue from the previous snippet in the same section unless they define those variables.Printing non-ASCII text requires an encoding layer on the output handle (
binmode STDOUT, ':encoding(UTF-8)'); the snippets leave it out for brevity. See "UTF-8 and Perl strings".
HOW TO FIND THINGS IN THIS DOCUMENT
Section titles use the vocabulary of the KDL specification, so searching for a spec term (slashdash, raw string, type annotation, multi-line string, line continuation, keyword) finds the right place. This index lists every feature with the section that covers it.
- annotated example of every construct
- node, node name, quoted name
- argument (positional value)
- property, key=value, quoted key, whitespace around =
- children block { }, node{} without a space, empty block
- newline terminator, semicolon ;
- line continuation \ (escline)
- (type) annotation on a node, reserved type names
- identifier string (bare string), identifier rules, reserved words
- "quoted string"
- escapes \n \t \u{...} \s, invalid escapes, surrogates
- whitespace escape (backslash followed by whitespace or newline)
- """ multi-line string, dedent, tabs versus spaces
- #"raw string"#, raw multi-line string
- r"..." raw strings, literal newlines (KDL v1)
- integer, 64-bit limits, kind => 'integer'
- float, exponent, kind => 'float'
- 0x 0o 0b radix prefixes
- 1_000 underscores in numbers
- big numbers, bigint, kind => 'string'
- keywords #inf #-inf #nan
- number kinds summary (integer, float, string), invalid numbers
-
"Number kinds: integer, float, string", "Invalid number forms"
- keywords #true #false, boolean
- keyword #null
- true false null without # (KDL v1)
- // single-line comment, /* */ multi-line comment
- /- slashdash comment, slashdash placement rules
- comment events, commented flag, streaming parser
- whitespace characters, newline characters, CRLF, vertical tab
- BOM, byte order mark
- control characters, DEL, bidi controls, disallowed code points
- UTF-8, encoding, character strings, binmode
- version detection, forcing v1 or v2, version marker
-
"Version detection", "Forcing KDL v1 or v2", "Version marker"
- differences between KDL v1 and v2 (table)
- converting a document between versions
- emitting plain Perl data (data mode), hash and array mapping
- emitting a tree (tree mode), what a round trip loses
-
"Emitting a parsed or hand-built tree", "What a round trip loses"
- building nodes and values by hand
- emitting booleans, type annotations, floats, keyword-like strings
-
"Emitting booleans", "Emitting type annotations", "Emitting floating point numbers", "Emitting strings that look like keywords or numbers"
- indent, escape_mode, identifier_mode
- recipes: config file, modify and write back, JSON, streaming, STDIN, errors, validation, untrusted input
-
"Load a configuration file into a hash", "Modify a document and write it back", "Convert KDL to JSON and back", "Stream a large file without building a tree", "Read from STDIN, a socket or a pipe", "Handle parse errors", "Validate against an expected structure", "Parse untrusted input"
KDL IN ONE PAGE
If you have never seen KDL before, this annotated document shows every construct at once. Each construct gets its own section below.
// A single-line comment. KDL is a tree of nodes.
package "kdl-rs" version="0.4.0" { // node "package": one argument, one property, children
author "Kat" email="kat@example.com" // arguments and properties can be mixed freely
keywords "config" "data" "structured" // any number of arguments
(published)date "2026-09-26" // a type annotation on the node
size (u32)1024 ratio=(f64)0.75 // type annotations on values
enabled #true debug=#false // booleans
description #null // null
counts 42 -7 0xFF 0o17 0b101 1_000 // integers in several radixes
ratios 3.14 1e3 -2.5e-3 // floats
big 123456789012345678901234567890 // arbitrary precision, kept as text
limits #inf #-inf #nan // special float values
bare hello-world // an identifier string, no quotes needed
escaped "tab\t newline\n quote\" \u{2713}"
multi """
Dedented multi-line
string
"""
raw #"no \escapes at "all""# // raw strings: backslashes are literal
long 1 2 \ // line continuation
3 4
one; two; three // semicolons also terminate nodes
/-disabled "this node is ignored" // slashdash comments out a whole node
partial 1 /-2 3 /-key=4 // or a single argument or property
}
Read it with:
use Text::KDL::XS qw(parse_kdl);
my $doc = parse_kdl($kdl);
for my $node (@{ $doc->nodes }) {
print $node->name, "\n";
}
DOCUMENT STRUCTURE
Nodes
A KDL document is a list of nodes. A node has a name, zero or more arguments, zero or more properties, and an optional block of child nodes. Nodes are separated by newlines (or semicolons, see "Node terminators: newlines and semicolons").
server "web-1" port=8080 {
tls #true
listen "0.0.0.0" "::"
upstream name="app" weight=3
}
Walk it from Perl:
use Text::KDL::XS qw(parse_kdl);
my $doc = parse_kdl($kdl);
for my $node (@{ $doc->nodes }) {
print "node: ", $node->name, "\n";
print " args: ", join(", ", map { $_->as_string } @{ $node->args }), "\n";
print " props: ", join(", ", map { "$_->[0]=" . $_->[1]->as_string } @{ $node->props }), "\n";
for my $child (@{ $node->children }) {
printf " child %-8s args=[%s] props={%s}\n", $child->name,
join(",", map { $_->as_string } @{ $child->args }),
join(",", map { "$_->[0]=" . $_->[1]->as_string } @{ $child->props });
}
}
Output:
node: server
args: web-1
props: port=8080
child tls args=[true] props={}
child listen args=[0.0.0.0,::] props={}
child upstream args=[] props={name=app,weight=3}
The same document as plain Perl data, via $doc->as_data:
[
{
name => 'server',
type => undef,
args => [ 'web-1' ],
props => { port => 8080 },
children => [
{ name => 'tls', type => undef, args => [ 1 ], props => {}, children => [] },
{ name => 'listen', type => undef, args => [ '0.0.0.0', '::' ], props => {}, children => [] },
{ name => 'upstream', type => undef, args => [],
props => { name => 'app', weight => 3 }, children => [] },
],
},
]
as_data is convenient but lossy: it drops type annotations on values, keeps only the last value of a repeated property, and turns booleans into 1 and 0. Use the object tree when you need those details.
Node names
A node name is a string. Most names are written bare (server, my-node, --flag), but any valid KDL string works when quoted, including strings with spaces, raw strings, and the empty string:
bare-name 1
"name with spaces" 2
"" 3
"123" 4
#"raw name"# 5
print join("|", map { $_->name } @{ parse_kdl($kdl)->nodes }), "\n";
# bare-name|name with spaces||123|raw name
$node->name always returns the decoded string without quotes. The same applies to property keys ("my key"=1) and to type annotations (("my type")node): see "Properties" and "Type annotations on nodes".
When emitting, "emit_kdl" in Text::KDL::XS quotes names automatically when they contain characters that are not allowed in a bare identifier, and quotes every identifier of the document when a name merely equals a keyword or looks like a number; see "Emitting strings that look like keywords or numbers".
Arguments
Arguments are positional values. Their order is significant and preserved.
my-node 1 2 3 "four" #true
my $node = parse_kdl($kdl)->nodes->[0];
my @args = @{ $node->args }; # five Text::KDL::XS::Value objects
print $args[3]->as_string; # four
print scalar @args; # 5
Properties
Properties are key=value pairs. The key is a string (bare or quoted), the value is any KDL value. Properties and arguments may be interleaved. KDL guarantees the order of arguments but tells readers not to rely on the order of properties; Text::KDL::XS nevertheless keeps properties in document order in props and writes them back in that order.
connect host="db" port=5432 "extra-arg" timeout=30 "my key"=1
my $node = parse_kdl($kdl)->nodes->[0];
# Lookup by name (returns undef if the property is absent):
print $node->prop('port')->as_number; # 5432
print defined $node->prop('nope') ? 'yes' : 'no'; # no
# Ordered list of [ key, Value ] pairs, in document order:
for my $pair (@{ $node->props }) {
my ($key, $value) = @$pair;
print "$key = ", $value->as_string, "\n";
}
# host = db
# port = 5432
# timeout = 30
# my key = 1
# Arguments are unaffected by the properties around them:
print $node->args->[0]->as_string; # extra-arg
In KDL v2 whitespace is allowed around the = (key = value); KDL v1 requires key=value without spaces. The emitter always writes key=value.
Duplicate properties
The KDL specification says that when a key appears more than once, the rightmost value wins. Text::KDL::XS follows that rule in prop and as_data, but keeps every occurrence in props so nothing is lost:
node a=1 a=2 b=3
my $node = parse_kdl($kdl)->nodes->[0];
print $node->prop('a')->as_number; # 2
print scalar @{ $node->props }; # 3 (a=1, a=2, b=3)
print join ",", sort keys %{ $node->as_data->{props} }; # a,b
print emit_kdl($node); # node a=1 a=2 b=3
Note that emit_kdl writes all occurrences back out. The result is valid KDL and parses to the same data.
Children blocks
A node may be followed by { ... } containing child nodes, to any depth. An empty block {} is allowed and is indistinguishable from no block (emit_kdl writes such a node without braces).
parent {
child1
child2 {
grandchild
}
}
compact { a; b; c }
empty {}
sub walk {
my ($node, $depth) = @_;
print " " x $depth, $node->name, "\n";
walk($_, $depth + 1) for @{ $node->children };
}
walk($_, 0) for @{ parse_kdl($kdl)->nodes };
Output:
parent
child1
child2
grandchild
compact
a
b
c
empty
$node->children is always an array reference, empty when the node has no block. The block ends the node: a { b } c is a syntax error, while a { b }; c and a newline after the } are fine. A block may also follow a line continuation (node 1 \ followed by { ... } on the next line).
KDL 2.0.0 requires whitespace between the node and its block; empty{} without the space is a syntax error. KDL 1.0.0 allows empty{}, but the underlying ckdl parser rejects it in v1 mode as well. Always write empty {}.
Node terminators: newlines and semicolons
A node ends at a newline, a semicolon, the closing } of its parent, or the end of the input. Semicolons let you put several nodes on one line:
a; b 1; c { d; e 2 }
last-line-without-newline
This parses to four top-level nodes (a, b, c, and last-line-without-newline); c has the two children d and e. The emitter always writes one node per line:
a
b 1
c {
d
e 2
}
last-line-without-newline
Line continuations
A backslash continues the node on the next line (the specification calls this an escline). Between the backslash and the newline there may be whitespace, a single-line comment, or a block comment (which may itself span lines); the newline that ends the continuation must still follow. A continuation at the very end of the input is allowed.
long-node 1 2 \
3 4 \ // a comment is fine here
key=5
The result is one node with arguments 1 2 3 4 and property key=5. Continuations are purely syntactic; emit_kdl writes everything on one line: long-node 1 2 3 4 key=5.
Type annotations on nodes
A node name may be prefixed with (type). The annotation is a free-form string (bare or quoted); KDL does not interpret it.
(published)date "2026-09-26"
(person)author "Kat"
("my type")thing 1
for my $node (@{ parse_kdl($kdl)->nodes }) {
printf "%-7s type=%s\n", $node->name, $node->type_annotation // 'undef';
}
# date type=published
# author type=person
# thing type=my type
type_annotation returns undef when the node has no annotation.
Type annotations on values
Arguments and property values can carry a (type) prefix as well. The specification lists annotations that implementations may recognise and says how they should be interpreted:
Numbers i8 i16 i32 i64 i128 u8 u16 u32 u64 u128 isize usize
f32 f64 decimal64 decimal128
Strings date-time time date duration decimal currency
country-2 country-3 country-subdivision
email idn-email hostname idn-hostname ipv4 ipv6
url url-reference irl irl-reference url-template
uuid regex base64 base85
Any other name is equally valid. Text::KDL::XS reports the annotation and leaves the value untouched whatever the name is.
temperature (f32)21.5 unit=(unit)"celsius"
author "Kat" id=(u64)42
for my $node (@{ parse_kdl($kdl)->nodes }) {
printf "arg %-8s type=%s\n", $_->as_string, $_->type_annotation // 'undef'
for @{ $node->args };
printf "prop %s=%-6s type=%s\n", $_->[0], $_->[1]->as_string, $_->[1]->type_annotation // 'undef'
for @{ $node->props };
}
# arg 21.5 type=f32
# prop unit=celsius type=unit
# arg Kat type=undef
# prop id=42 type=u64
In KDL v2 whitespace is permitted inside the parentheses and between the annotation and its value (( u8 ) 42); KDL v1 permits neither. The emitter always writes the compact form (u8)42.
Annotations survive a round trip through emit_kdl in tree mode, and can be attached to values you build yourself (see "Emitting type annotations").
STRINGS
KDL v2 has three forms of string: identifier strings, quoted strings and multi-line strings. Quoted and multi-line strings each have a raw variant that disables escapes. All forms produce the same kind of value: $value->type is 'string' and $value->as_string returns the decoded text. This document uses the specification's term identifier string; other documents call the same thing a bare string or bare identifier.
Identifier strings
An identifier string is a string without quotes. The KDL v2 rules:
Allowed any Unicode character except the ones below
Not allowed whitespace, newlines, the disallowed characters listed in
"Disallowed characters", and these twelve characters:
( ) { } [ ] / \ " # ; =
First char must not be a digit
+ or - may start an identifier only if what follows is
not a digit, and not a . followed by a digit
(so -flag is fine, -1 and -.5 are not identifiers)
. may start an identifier only if the next character is
not a digit
Reserved true false null inf -inf nan (write them quoted)
node hello-world --flag path.to.x=1
The node has two arguments (hello-world, --flag) and one property (path.to.x = 1). As a Perl string an identifier is indistinguishable from a quoted string: parse_kdl('n a')->nodes->[0]->args->[0]->as_string and parse_kdl('n "a"') ... both give a.
KDL v1 has its own rules: bare identifiers are only allowed as node names and property keys, never as values (node foo is a syntax error in v1); the characters <, > and , are forbidden, # is allowed; only true, false and null are reserved, so inf and nan are ordinary v1 identifiers.
Parser quirk: in the default detection mode, .5, -.5 and +.5 are accepted as identifier strings although neither version allows them (both version => '1' and version => '2' reject them).
Quoted strings
Quoted strings are delimited by double quotes and support escape sequences. A literal newline inside the quotes is a syntax error in KDL v2. To spread a long string over several source lines, escape the newline with a backslash, which removes it from the value (see "Whitespace escapes"); to put a newline into the value, write \n or use a multi-line string.
greeting "Hello, world"
path "C:\\Users\\kat"
tab "col1\tcol2"
my %by_name = map { $_->name => $_->args->[0]->as_string } @{ parse_kdl($kdl)->nodes };
print $by_name{path}; # C:\Users\kat (one backslash each)
print $by_name{tab}; # col1<TAB>col2
Escape sequences
Inside quoted (non-raw) strings the following escapes are recognised:
Escape Meaning
--------- --------------------------------------------------------
\n line feed (U+000A)
\r carriage return (U+000D)
\t tab (U+0009)
\\ backslash
\" double quote
\b backspace (U+0008)
\f form feed (U+000C)
\s space (U+0020) (KDL v2 only)
\/ forward slash (KDL v1 only)
\u{X..} Unicode scalar value, 1 to 6 hex digits, up to \u{10FFFF}
Any other character after a backslash is a syntax error, and so is a code point above U+10FFFF. The specification also forbids surrogates (\u{D800} to \u{DFFF}); they are rejected in every version mode (in v1 and detection mode with KDL parse error: string contains a surrogate or a code point above U+10FFFF). The underlying parser is lenient about the digits: \u{} with no digits at all is accepted and yields U+0000, although the specification requires at least one digit, and more than six digits wrap around (\u{1000000041} is A).
quoted "tab\there, quote\", backslash\\, unicode \u{2713}, space\sgap"
controls "\b\f\r"
max "\u{10FFFF}"
my %by_name = map { $_->name => $_->args->[0]->as_string } @{ parse_kdl($kdl)->nodes };
print $by_name{quoted}; # tab<TAB>here, quote", backslash\, unicode ✓, space gap
print join ",", map { ord } split //, $by_name{controls}; # 8,12,13
printf "%X\n", ord $by_name{max}; # 10FFFF
When emitting, emit_kdl escapes ", \, tabs, newlines and other control characters, and leaves other Unicode characters as they are. escape_mode changes that (see "Escaping and identifier quoting options").
Whitespace escapes
In KDL v2 a backslash followed by literal whitespace (including newlines) removes the backslash and all of that whitespace. This is how a quoted string can be split across lines without adding a newline to the value:
continued "one \
two"
The value is one two. Escape sequences such as \n are not whitespace and are kept. KDL v1 has no whitespace escape; the same input is a syntax error with version => '1'.
Multi-line strings
Three double quotes start a multi-line string. The opening """ must be followed by a newline, the closing """ must be on its own line, and the whitespace before the closing quotes defines the indentation that is removed from every line (dedent).
multi """
Dear reader,
indented line
bye
"""
The value is "Dear reader,\n indented line\nbye": four spaces were removed from each line, the two extra spaces on the middle line remain, and neither the first nor the last newline is part of the value. Line endings inside the string are normalised to \n, so a CRLF file yields the same value. Whitespace-only lines become empty lines regardless of their indentation.
The prefix must match exactly, character for character: a line indented with a tab where the closing line uses spaces is a syntax error, not a different amount of indentation. A multi-line string may be empty (""" on one line and """ on the next). Escape sequences work as in quoted strings, and whitespace escapes (a backslash before a newline) are applied before dedenting.
emit_kdl never writes multi-line strings; it writes the value as a single-line quoted string with \n escapes:
multi "Dear reader,\n indented line\nbye"
Multi-line strings with """ are KDL v2 syntax. In v1 a plain quoted string may contain literal newlines instead (see "KDL v1 strings").
Raw strings
A raw string is a quoted string prefixed with one or more #. Backslashes have no special meaning inside it. The string ends at a " followed by the same number of #, so choose enough hashes that the terminator cannot appear inside the content.
raw #"C:\path\no "escapes" here"#
raw-more ##"contains "# inside"##
Values: C:\path\no "escapes" here and contains "# inside.
Because there are no escapes, a raw string cannot contain the characters KDL forbids literally (see "Disallowed characters"); use a quoted string with \u{...} for those.
emit_kdl never writes raw strings; it re-escapes the value instead (raw "C:\\path\\no \"escapes\" here"). The parsed value is the same.
Raw multi-line strings
Raw and multi-line combine:
raw-multi #"""
literal \n stays
"""#
The value is literal \n stays with a real backslash and n. Dedent rules are the same as for "Multi-line strings".
KDL v1 strings
KDL v1 (parse with version => '1', or rely on detection) differs in five ways:
Raw strings are written
r"...",r#"..."#and so on, with anrprefix instead of a leading#.Quoted and raw strings may contain literal newlines; there is no
"""syntax and no dedenting.\/is a valid escape (for/);\sis not.There is no whitespace escape (backslash before a newline).
Bare identifiers are not values.
node foois a syntax error in v1 (in v2 it is the stringfoo).
raw r"C:\path"
raw-hash r#"has "quotes""#
multi "line one
line two"
slash "a\/b"
Parsed with version => '1' the values are C:\path, has "quotes", "line one\nline two" and a/b.
NUMBERS
KDL has a single number type. Text::KDL::XS reports every number with $value->type eq 'number' and distinguishes three storage kinds via $value->kind: integer (a Perl integer), float (a Perl floating point number) and string (the digits kept as text). The exact rules are in "Number kinds: integer, float, string"; the sections before it show them in action.
The examples in this part all use this loop:
for my $node (@{ parse_kdl($kdl)->nodes }) {
print $node->name, "\n";
printf " kind=%-8s value=%s\n", $_->kind, $_->value for @{ $node->args };
}
Integers
integers 42 -7 +3 1_000_000 007
limits 9223372036854775807 -9223372036854775808
unsigned 9223372036854775808 18446744073709551615
integers
kind=integer value=42
kind=integer value=-7
kind=integer value=3
kind=integer value=1000000
kind=integer value=7
limits
kind=integer value=9223372036854775807
kind=integer value=-9223372036854775808
unsigned
kind=integer value=9223372036854775808
kind=integer value=18446744073709551615
Every integer from -2**63 to 2**64-1 is kind => 'integer' with a Perl integer in value; values above 2**63-1 are unsigned integers (UV). emit_kdl writes all of them back exactly. Anything beyond that range keeps its digits as kind => 'string', see "Arbitrary precision numbers".
Floating point numbers
A number with a decimal point or an exponent is a float. It comes back as kind => 'float' when at most 15 digits are written before the exponent (leading and trailing zeros count, underscores do not) and the written exponent lies between -284 and 284; otherwise the exact text is kept:
floats 3.14 -0.5 1e3 6.02e23 1.5E-4 1E+3 +1.5 -0.0
precise 3.141592653589793 0.30000000000000004 1e285 1e400
floats
kind=float value=3.14
kind=float value=-0.5
kind=float value=1000
kind=float value=6.02e+23
kind=float value=0.00015
kind=float value=1000
kind=float value=1.5
kind=float value=0
precise
kind=string value=3.141592653589793
kind=string value=0.30000000000000004
kind=string value=1e285
kind=string value=1e400
value is a Perl floating point number for the float kind, the double nearest to the literal, so 1e3 comes back as 1000 and prints without a decimal point (and -0.0 prints as 0, although it keeps its sign). When emitted again the float kind is remembered: emit_kdl writes the first node as floats 3.14 -0.5 1000.0 6.02e+23 0.00015 1000.0 1.5 -0.0, every float with the shortest text that reads back as the same double. A string kind number is written back verbatim.
Note that 3.141592653589793 (16 digits) is a string, and so are 1.000000000000000 and 123456789012345.0 (16 digits including the zeros), so a double printed with full precision is not a float on the way back in; $v->as_number gives the double and $v->as_bignum the exact decimal. See "Emitting floating point numbers" for writing floats.
Hexadecimal, octal and binary numbers
radix 0xFF 0o755 0b1010 0x7FFF_FFFF 0xDEAD_BEEF
radix
kind=integer value=255
kind=integer value=493
kind=integer value=10
kind=integer value=2147483647
kind=integer value=3735928559
Radix prefixes exist only in the source text; parsed values are ordinary numbers and emit_kdl writes them in decimal (radix 255 493 10 2147483647 3735928559). Hexadecimal digits may be upper or lower case, but the prefix must be lower case: 0XFF is a syntax error. A sign may precede the prefix: -0x10 is -16.
Underscore digit separators
Underscores may appear between digits, or after them, in any number syntax: 1_000_000, 0xDEAD_BEEF, 1_0.5_0, 12____. They are ignored. An underscore before the first digit is not allowed: _12 is an identifier string, and 0x_FF and 1e_10 are syntax errors.
Arbitrary precision numbers
KDL puts no limit on the size of a number. When a value does not fit the native C types, or when ckdl cannot guarantee an exact conversion (see the rules under "Number kinds: integer, float, string"), Text::KDL::XS keeps the text:
huge 123456789012345678901234567890 1e400
extremes 18446744073709551615 18446744073709551616
huge
kind=string value=123456789012345678901234567890
kind=string value=1e400
extremes
kind=integer value=18446744073709551615
kind=string value=18446744073709551616
The text is normalised (underscores removed, radix prefixes converted to decimal, a leading + dropped), but otherwise verbatim, and is written back out unchanged by emit_kdl. To compute with it exactly, ask for a Math::BigInt or Math::BigFloat:
my $v = parse_kdl($kdl)->nodes->[0]->args->[0];
my $exact = $v->as_bignum; # Math::BigInt 123456789012345678901234567890
print $exact * 2; # 246913578024691357802469135780
$v->as_number converts the text to a Perl number, which is fine for a quick comparison but loses precision (1.23456789012346e+29), and 1e400 becomes infinite.
Keyword numbers: #inf, #-inf and #nan
KDL v2 spells the IEEE special values #inf, #-inf and #nan:
keywords #inf #-inf #nan
keywords
kind=float value=Inf
kind=float value=-Inf
kind=float value=NaN
They come back as Perl's Inf, -Inf and NaN and are written as #inf #-inf #nan by emit_kdl. KDL v1 has no syntax for them, so emit_kdl($doc, version => '1') dies with emit_kdl: KDL v1 has no representation for inf/nan rather than writing an invalid document.
Number kinds: integer, float, string
Summary of $value->kind for a number value:
kind value is when
--------- -------------------------------- ---------------------------------------
integer Perl integer (IV, or UV above no . or exponent; value within
2**63-1) -2**63 .. 2**64-1
float Perl floating point number (NV) has . or exponent; at most 15 digits
written before the exponent (zeros
count); written exponent within
-284 .. 284; also #inf, #-inf, #nan
string the digits as text every other number
The kind is preserved through emit_kdl in tree mode, so a document with 1.0 is written back as 1.0, not 1, and a string kind is copied verbatim.
Invalid number forms
These are syntax errors: .5 and 5. (a digit is required on both sides of the point; 0.5 and 5.0 are fine), 0XFF (upper-case prefix), 0x_FF and 1e_10 (underscore before the digits), and 1abc or -1em (a number followed by letters). See "Identifier strings" for the detection-mode quirk that accepts .5.
BOOLEANS AND NULL
Keywords #true and #false
KDL v2 writes booleans as #true and #false, as arguments or property values:
flags #true #false enabled=#true
my $node = parse_kdl($kdl)->nodes->[0];
for my $v (@{ $node->args }) {
printf "type=%s is_bool=%d as_perl=%s as_string=%s\n",
$v->type, $v->is_bool, $v->as_perl, $v->as_string;
}
# type=bool is_bool=1 as_perl=1 as_string=true
# type=bool is_bool=1 as_perl=0 as_string=false
print $node->prop('enabled')->type; # bool
as_perl and value give 1 or 0, as_string gives true or false. Booleans are not confused with the strings "true" and "false": those have type eq 'string'.
To emit a boolean from plain Perl data you must pass a boolean object, because Perl's 1 and 0 are numbers. See "Emitting booleans".
Keyword #null
description #null key=#null
my $node = parse_kdl($kdl)->nodes->[0];
print $node->args->[0]->is_null; # 1
print defined $node->args->[0]->as_perl ? 1 : 0; # 0
print $node->prop('key')->type; # null
value, as_perl, as_string and as_number all return undef for null. To emit null, use undef in data mode or Text::KDL::XS::Value->new(type => 'null') in tree mode.
KDL v1 keywords: true, false, null
KDL v1 writes them without the #: true, false, null. The parsed values are identical.
flags true false null
Parsed with the default version detection or version => '1', the three arguments are bool, bool, null. emit_kdl writes them in whichever syntax you ask for: flags #true #false #null by default, flags true false null with version => '1'.
COMMENTS
Comments are discarded by parse_kdl. Slashdash comments affect the data (they remove things). The streaming parser can report all of them; see "Reading comments with the streaming parser".
Single-line comments
// starts a comment that runs to the end of the line:
// A single-line comment
title "Example" // trailing comment
Multi-line comments
/* ... */ comments may span lines, may appear anywhere whitespace may appear (even inside a node between arguments or inside a children block), and nest:
/* A block comment /* nested */ still a comment */
title "Example" /* inline */ draft=#false
section {
/* spans
two lines */ child 1
}
my $doc = parse_kdl($kdl);
print scalar @{ $doc->nodes }; # 2
print join ",", map { $_->as_perl } @{ $doc->nodes->[0]->args }; # Example
print $doc->nodes->[1]->children->[0]->name; # child
print emit_kdl($doc); # no comments in the output
Slashdash comments
/- comments out the next element as a whole: a node (including all of its entries and children), a single argument, a single property, or a children block. If the element carries a type annotation the slashdash goes before the annotation (/-(t)1; (t)/-1 is an error).
/-disabled "this whole node is ignored" {
child 1
}
mixed 1 /-2 3 key=1 /-gone=2 /-{
also-ignored
}
annotated /-(t)1 2
my $doc = parse_kdl($kdl);
print scalar @{ $doc->nodes }; # 2 (disabled is gone)
my $mixed = $doc->nodes->[0];
print join ",", map { $_->as_perl } @{ $mixed->args }; # 1,3
print join ",", map { $_->[0] } @{ $mixed->props }; # key
print scalar @{ $mixed->children }; # 0
print $doc->nodes->[1]->args->[0]->as_perl; # 2
A slashdash may be followed by whitespace, newlines and ordinary comments before the element it removes. A slashdash cannot remove only the value of a property (key=/-1 is an error). After a slashdashed children block only another children block may follow (node /-{ a } { b } keeps b; node /-{ a } 2 is an error).
Reading comments with the streaming parser
Text::KDL::XS::Parser can be asked to report comments and slashdashed elements with emit_comments => 1. Every event has a commented key (also without the option, where it is always 0); with the option it is 1 for slashdashed elements, and ordinary comments arrive as comment events.
use Text::KDL::XS::Parser;
my $p = Text::KDL::XS::Parser->new("// note\n$kdl", emit_comments => 1);
while (my $ev = $p->next_event) {
printf "%-10s commented=%d %s\n", $ev->{event}, $ev->{commented},
$ev->{name} // $ev->{text} // '';
}
For a comment followed by the slashdash example above this prints:
comment commented=1 // note
start_node commented=1 disabled
argument commented=1
start_node commented=1 child
argument commented=1
end_node commented=1
end_node commented=1
start_node commented=0 mixed
argument commented=0
argument commented=1
argument commented=0
property commented=0 key
property commented=1 gone
start_node commented=1 also-ignored
end_node commented=1
end_node commented=0
start_node commented=0 annotated
argument commented=1
argument commented=0
end_node commented=0
A comment event carries the comment as written in text, delimiters included (// note, /* ... */), but no position; its place in the event sequence shows where the comment stood.
parse_kdl accepts emit_comments as well, but comments and slashdashed elements never become part of the tree it builds.
WHITESPACE, NEWLINES AND ENCODING
Whitespace characters
Space, tab, and the Unicode space characters listed in the specification (U+00A0, U+1680, U+2000 to U+200A, U+202F, U+205F, U+3000) separate tokens. They carry no meaning and are not preserved:
my $doc = parse_kdl("a\t1\x{a0}2 3\n"); # tab, no-break space, spaces
print scalar @{ $doc->nodes->[0]->args }; # 3
print emit_kdl($doc); # a 1 2 3
Newline characters
CRLF, CR, LF, U+0085 (NEL), U+000C (FF), U+2028 (LS) and U+2029 (PS) all terminate a node. CRLF counts as one newline, so documents from Windows parse without conversion. emit_kdl always writes LF.
my $doc = parse_kdl("a 1\r\nb 2\rc 3\n");
print join ",", map { $_->name } @{ $doc->nodes }; # a,b,c
KDL v2 also lists the vertical tab (U+000B) as a newline; the underlying parser treats it as ordinary whitespace instead (KDL v1 rejects it).
Byte order mark
A UTF-8 byte order mark (U+FEFF) at the very start of the document is skipped. In KDL v2 it is a syntax error anywhere else; KDL v1 treats it as whitespace everywhere.
my $doc = parse_kdl("\x{feff}node 1\n"); # a string starting with the BOM character
print $doc->nodes->[0]->name; # node
The same holds for the three bytes EF BB BF at the start of a file read through a filehandle.
Disallowed characters
Some code points may not appear literally anywhere in a document, not even inside strings: the control characters U+0000 to U+0008 and U+000E to U+001F, DEL (U+007F), the Unicode direction-control characters (U+200E, U+200F, U+202A to U+202E, U+2066 to U+2069), surrogates, and a BOM anywhere but at the start (in v2). The parser rejects them with a parse error in v2 and detection mode; with version => '1' it accepts them inside strings and identifiers. Quoted strings can hold them through \u{...} escapes (except surrogates, which are never allowed, see "Escape sequences"); raw strings cannot.
bell "a\u{7}b"
$node->args->[0]->as_string is a, the BEL character, b. The same three characters typed literally between the quotes are a syntax error.
UTF-8 and Perl strings
KDL documents are UTF-8. parse_kdl and Text::KDL::XS::Parser take a string as Perl characters, read filehandles and code references as UTF-8 bytes (characters from an encoding layer or a code reference are encoded for you), and return Perl character strings (with the UTF-8 flag set, so length counts characters). emit_kdl takes character strings and returns a character string, which you encode when writing to a file or socket, and which print mangles unless the output handle has an encoding layer. So parse_kdl(emit_kdl($data)) always works.
use utf8; # string literals in this file are characters
use Encode qw(encode decode);
binmode STDOUT, ':encoding(UTF-8)';
# Reading: pass the filehandle, any layer works ...
open my $in, '<', 'config.kdl' or die $!;
my $doc = parse_kdl($in);
my $name = $doc->nodes->[0]->name; # a character string, e.g. "café"
print length $name; # 4
# ... or pass a character string.
my $inline = parse_kdl("café 1\n"); # a literal under use utf8
my $bytes = do { open my $raw, '<:raw', 'config.kdl' or die $!; local $/; <$raw> };
my $again = parse_kdl(decode('UTF-8', $bytes)); # bytes must be decoded first
# Writing: encode the emitted characters.
open my $out, '>:raw', 'config.kdl' or die $!;
print {$out} encode('UTF-8', emit_kdl($doc));
# Or let a layer do it:
open my $out2, '>:encoding(UTF-8)', 'config.kdl' or die $!;
print {$out2} emit_kdl($doc);
The one thing to watch is a string of UTF-8 bytes (a heredoc in a source file without use utf8, or data slurped through :raw): it has to be decoded before parse_kdl sees it, otherwise every byte counts as one character and café comes back as five characters instead of four. Text::KDL::XS 0.001 expected exactly such byte strings; code written for it must decode now.
Input that is not valid UTF-8 (including overlong forms, surrogates and code points above U+10FFFF) is a parse error, KDL parse error: input is not valid UTF-8. On output, emit_kdl dies for a name, key or string that contains a surrogate or a code point above U+10FFFF, which no KDL document can hold.
KDL VERSIONS
There are two versions of KDL. Version 2.0.0 is current; 1.0.0 is the legacy format. The main visible differences are the # prefix on #true, #false, #null, the availability of bare identifier strings as values, and the new """ and #"..."# string syntaxes. The full list is in "Differences between KDL v1 and v2".
Version detection
By default (version => 'detect') the parser accepts both versions and decides from the first version-specific construct it meets. For a document that uses only the common subset (quoted strings, numbers, node names) both readings are the same.
my $doc = parse_kdl('node true'); # detected as v1: argument is a boolean
my $doc = parse_kdl('node #true'); # detected as v2: argument is a boolean
my $doc = parse_kdl('node "same"'); # either: argument is the string "same"
Once a version has been detected, constructs from the other version are syntax errors. Mixing true and #true in one document fails.
The parser does not report which version it settled on. When you need to know, parse with version => '2' first and fall back to version => '1':
my ($doc, $version);
for my $try ('2', '1') {
$doc = eval { parse_kdl($text, version => $try) } or next;
$version = $try;
last;
}
die "not valid KDL: $@" unless $doc;
Forcing KDL v1 or v2
Pass version => '1' or version => '2' to accept only one syntax. This table shows how six one-line documents fare under each setting (the cell gives the type of the argument; ERROR means parse_kdl dies):
Document detect version=1 version=2
-------------------- -------- ---------- ----------
node "v1 or v2" string string string
node true bool bool ERROR
node #true bool ERROR bool
node r"raw" string string ERROR
node #"raw"# string ERROR string
node bare-ident string ERROR string
The underlying ckdl library documents detection as exact for every v2 document and for almost every v1 document. If strict v1 conformance matters, pass version => '1'.
Version marker
A document may start with the version marker /- kdl-version 2 (or 1). The specification defines it as a hint that parsers may use. It is written with slashdash syntax, so this parser simply removes it like any other slashdashed node and does not use it to choose the version; it is harmless and helps readers.
/- kdl-version 2
node #true
Differences between KDL v1 and v2
Feature KDL v1 KDL v2
----------------------------- --------------------------- ----------------------------
booleans true false #true #false
null null #null
infinity, NaN (not representable) #inf #-inf #nan
bare strings as values not allowed allowed (identifier strings)
reserved identifiers true false null also inf -inf nan
characters in identifiers < > , forbidden; # allowed # forbidden; < > , allowed
raw strings r"..." r#"..."# #"..."# ##"..."##
multi-line strings literal newlines in "..." """ ... """ with dedent
\s escape (space) no yes
\/ escape (slash) yes no
whitespace escape (\ newline) no yes
whitespace around = and (type) not allowed allowed
node{} without a space allowed by the spec not allowed
(rejected by this parser)
BOM after the start whitespace syntax error
vertical tab not allowed newline (whitespace in this
parser)
Converting between versions
Parse with detection, emit with an explicit version:
my $doc = parse_kdl($v1_text);
my $v2 = emit_kdl($doc, version => '2'); # #true, bare identifiers, ...
my $v1 = emit_kdl($doc, version => '1'); # true, everything quoted, ...
Given flags true false null the v2 output is flags #true #false #null. Given the v2 document
node "tab\there" key=1 { child value }
the v1 output is
node "tab\there" key=1 {
child "value"
}
because v1 has to quote every string value.
WRITING KDL
"emit_kdl" in Text::KDL::XS has two modes. It picks the mode from its first argument: a Text::KDL::XS::Document, a Text::KDL::XS::Node or an array reference of nodes selects tree mode; any other hash or array reference selects data mode.
Emitting plain Perl data
Data mode turns nested hashes and arrays into nodes. Every hash key becomes a node; data mode never writes properties.
Perl value KDL output
---------------------------- -------------------------------------------
{ key => $scalar } key <value>
{ key => undef } key #null
{ key => [ $s1, $s2 ] } key <s1> <s2> (all elements scalars)
{ key => [] } key (bare node)
{ key => {} } key (bare node)
{ key => { ... } } key { ...children... }
{ key => [ {..}, {..} ] } key { ... } key { ... } (one sibling per element)
{ key => [ $s, {..} ] } key <s> key { ... } (mixed: one sibling per element)
{ key => [ [1,2], [3] ] } key 1 2 key 3 (inner arrays: one sibling each)
[ $a, $b ] (top level) - <a> - <b> (nodes named "-")
{} or [] (top level) (a single newline)
boolean object #true / #false
Text::KDL::XS::Value emitted with its type, kind and annotation
Hash keys are sorted so output is deterministic.
print emit_kdl({
title => 'Data mode',
retries => 3,
ratio => 0.5,
nothing => undef,
tags => [ 'a', 'b', 'c' ],
empty => [],
server => { host => 'localhost', port => 8080 },
user => [ { name => 'kat', admin => 1 }, { name => 'sam' } ],
quoted => 'two words',
number => '42',
});
empty
nothing #null
number "42"
quoted "two words"
ratio 0.5
retries 3
server {
host localhost
port 8080
}
tags a b c
title "Data mode"
user {
admin 1
name kat
}
user {
name sam
}
Note number "42": the Perl string '42' is emitted as a string, and the Perl number 42 as a number. A string that has been used as a number counts as a number only when its text is exactly how Perl prints that number ('42' does, '042' and '4.20' do not), so no text is ever altered; see "Scalar coercion" in Text::KDL::XS. Wrap in "$x" or 0 + $x when it matters. A hash or array that contains itself makes emit_kdl die (emit_kdl: cyclic data structure).
A top-level array produces anonymous nodes named -, the convention used by KDL's JSON mapping:
print emit_kdl([ 'x', 2, { k => 'v' } ]);
- x
- 2
- {
k v
}
Data mode is lossy by design: it cannot express properties, arguments and children on the same node, a specific node order (keys are sorted), type annotations on nodes, or the difference between { key => 'a' } and { key => ['a'] } (both give key a). Type annotations on values are possible by embedding Text::KDL::XS::Value objects. Use tree mode for everything else.
Emitting a parsed or hand-built tree
Tree mode writes exactly what the objects contain: argument order, property order, type annotations, number kinds.
my $doc = parse_kdl($text);
print emit_kdl($doc); # whole document
print emit_kdl($doc->nodes->[0]); # one node (and its subtree)
print emit_kdl([ grep { $_->name eq 'server' } @{ $doc->nodes } ]); # a selection
An array reference is treated as tree mode only when every element is a Text::KDL::XS::Node (or a subclass); an array that mixes nodes with other values dies (emit_kdl: cannot serialize Text::KDL::XS::Node object), and an empty array reference emits a single newline.
What a round trip loses
parse_kdl followed by emit_kdl in tree mode preserves the data of the document: node names and order, arguments and their order, properties and their order (including repeated keys), children, type annotations, the value of every string, and the kind and value of every number. It does not preserve the source text. Gone are:
comments of every kind, and slashdashed elements;
layout: indentation, blank lines, semicolons, line continuations, whitespace around
=and inside type annotations;the spelling of strings: raw and multi-line strings become escaped single-line strings, identifier strings stay bare only if the emitter considers that safe;
the spelling of numbers:
0xFFbecomes255,1_000becomes1000,1e3becomes1000.0,6.02e23becomes6.02e+23,+3becomes3;empty children blocks (
node {}becomesnode);the KDL version, unless you pass
versiontoemit_kdl.
The value of every float is kept exactly: it is written with the shortest text that reads back as the same double.
Building nodes and values by hand
use Text::KDL::XS qw(emit_kdl); # also loads Document, Node and Value
my $str = sub { Text::KDL::XS::Value->new(type => 'string', value => shift) };
my $int = sub { Text::KDL::XS::Value->new(type => 'number', value => shift) };
my $version = Text::KDL::XS::Node->new(name => 'version', args => [ $str->('1.0') ]);
my $package = Text::KDL::XS::Node->new(
name => 'package',
type_annotation => 'cargo',
args => [ $str->('kdl-rs') ],
props => [ [ edition => $int->(2021) ] ],
children => [ $version ],
);
print emit_kdl(Text::KDL::XS::Document->new(nodes => [ $package ]));
(cargo)package kdl-rs edition=2021 {
version "1.0"
}
The number Value above gets its kind (integer) from the Perl value; pass kind => 'string' to write a number with exactly the digits you give. The constructors check their arguments, so a mistake such as type => 'Number' or a non-numeric number dies where the object is made, not later in emit_kdl.
Plain scalars are also accepted inside args and props of a hand-built node and are coerced like data-mode values: Text::KDL::XS::Node->new(name => 'n', args => [ 1, 'two', undef ]) emits n 1 two #null. $node->prop($key) works on hand-built nodes as well; it searches props, the rightmost occurrence of a key winning.
Emitting booleans
Perl has no boolean type, so 1 and 0 are emitted as numbers and the strings 'true' and 'false' as strings. To get #true and #false pass a boolean object from JSON::PP, Types::Serialiser, boolean or Mojo::JSON, or a Text::KDL::XS::Value of type bool (whose value is judged by Perl truthiness, so 'false' would be true):
use JSON::PP ();
print emit_kdl({ enabled => JSON::PP::true(), verbose => JSON::PP::false() });
# enabled #true
# verbose #false
my $yes = Text::KDL::XS::Value->new(type => 'bool', value => 1);
print emit_kdl({ flag => $yes }); # flag #true
print emit_kdl({ enabled => JSON::PP::true(), literally => 'true' });
# "enabled" #true
# "literally" "true"
The string 'true' stays a string. Written bare it would read back as a boolean, so emit_kdl quotes it, and with it every identifier of the document; see "Emitting strings that look like keywords or numbers".
Emitting type annotations
Wrap the value in a Text::KDL::XS::Value with type_annotation. This works in data mode and tree mode:
print emit_kdl({
when => Text::KDL::XS::Value->new(type => 'string', value => '2026-09-26', type_annotation => 'date'),
count => Text::KDL::XS::Value->new(type => 'number', kind => 'integer', value => 7, type_annotation => 'u8'),
});
# count (u8)7
# when (date)"2026-09-26"
Node type annotations are set with type_annotation in "new" in Text::KDL::XS::Node.
Emitting floating point numbers
Floats are written with the shortest text that reads back as exactly the same double, and always with a decimal point or an exponent, so that they stay floats when parsed again. Perl integers are written as integers:
print emit_kdl({ n => 0.1 + 0.2 }); # n 0.30000000000000004
print emit_kdl({ n => 123456789.0 }); # n 123456789.0
print emit_kdl({ n => 1908124443056.387 }); # n 1908124443056.387
print emit_kdl({ n => 1e21 }); # n 1e+21
print emit_kdl({ n => -0.0 }); # n -0.0
print emit_kdl({ n => 123456789 }); # n 123456789 (an integer)
Infinity and NaN are written as #inf, #-inf and #nan in KDL v2 and make emit_kdl die in KDL v1, which cannot express them.
To write a number with exactly the digits you choose, for example a price with trailing zeros or a decimal with more precision than a double, hand it over as a string-encoded number, which is checked for KDL number syntax and copied verbatim:
my $price = Text::KDL::XS::Value->new(type => 'number', kind => 'string', value => '1.50');
print emit_kdl({ price => $price }); # price 1.50
A value with 16 or more significant digits comes back from parse_kdl as kind => 'string' (see "Floating point numbers"); its text is exact, $v->as_number gives the double and $v->as_bignum the exact decimal.
Emitting strings that look like keywords or numbers
The emitter writes node names, property keys and type annotations bare whenever the characters allow it, and in KDL v2 output does the same for string values. Written bare, the reserved words true, false, null (and in v2 inf, -inf, nan) and number-like text such as -1, +1 or .5 would read back as keywords or numbers, or not at all. When a document contains such a string, emit_kdl therefore writes it with every identifier quoted (identifier_mode => 1):
print emit_kdl({ answer => 'yes' }); # answer yes
print emit_kdl({ answer => 'true' }); # "answer" "true"
print emit_kdl({ n => '-1' }); # "n" "-1"
print emit_kdl({ true => 6 }, version => '1'); # "true" 6
The underlying ckdl library decides quoting from the characters alone, which is why the whole document is switched rather than the one string. If you pass identifier_mode yourself, it is used as given: with mode 0 or 2 such strings are written bare and the output does not round-trip (emit_kdl({ answer => 'true' }, identifier_mode => 0) gives answer true).
Indentation
Children are indented by four spaces. indent sets a different width:
my $doc = parse_kdl("node key=1 {\n child (t)value\n}\n");
print emit_kdl($doc, indent => 2);
node key=1 {
child (t)value
}
Escaping and identifier quoting options
escape_mode controls which characters inside quoted strings are written as escape sequences. It is a bit mask; the useful values are:
escape_mode Effect
----------- ---------------------------------------------------------
0 minimal: " and \ (v2 output always escapes the characters
KDL forbids literally: U+0000 to U+0008, U+000E to U+001F,
U+007F, bidi controls, U+FEFF)
0x10 also escape backspace and vertical tab
0x20 also escape newline characters (LF, CR, FF, NEL, LS, PS)
0x40 also escape tabs
0x70 the default: control characters, newlines and tabs
0x170 ASCII only: additionally escape every non-ASCII character
The flags 0x10, 0x20 and 0x40 may be combined with bitwise or; 0x100 only has an effect together with all of 0x70, and 0x170 is the preset for ASCII-only output. KDL v2 does not allow a literal newline inside a quoted string, so v2 output always escapes newlines, whatever escape_mode says; in v1 output a mode without 0x20 writes the newline literally.
identifier_mode controls quoting of node names, property keys, type annotations and (in v2) string values:
identifier_mode Effect
--------------- ---------------------------------------------------
0 bare when the characters allow it
1 quote every identifier and every string
2 bare only if pure ASCII, otherwise quoted
(not given) mode 0, or mode 1 when some string would not
read back as written (see above)
Given the document
node "tab\there" "ünïcode ✓" key=1 {
child (t)value
}
print emit_kdl($doc, escape_mode => 0x170);
# node "tab\there" "\u{fc}n\u{ef}code \u{2713}" key=1 {
# child (t)value
# }
print emit_kdl($doc, escape_mode => 0);
# node "tab<TAB>here" "ünïcode ✓" key=1 {
# child (t)value
# }
print emit_kdl($doc, identifier_mode => 1);
# "node" "tab\there" "ünïcode ✓" "key"=1 {
# "child" ("t")"value"
# }
RECIPES
Load a configuration file into a hash
A small recursive helper that maps the common configuration idioms: one argument becomes a scalar, several arguments become an array, properties become hash entries, and children become a nested hash. It is deliberately simple and lossy: a node that has children contributes only its children (its own arguments and properties are dropped), a repeated child name keeps the last occurrence, and repeated properties keep the last value. Adjust it to the shape of your configuration.
use Text::KDL::XS qw(parse_kdl);
sub node_to_hash {
my ($node) = @_;
my %h;
for my $child (@{ $node->children }) {
my @args = map { $_->as_perl } @{ $child->args };
my %props = map { $_->[0] => $_->[1]->as_perl } @{ $child->props };
my $value = @{ $child->children } ? node_to_hash($child)
: %props ? { args => \@args, %props }
: @args == 1 ? $args[0]
: \@args;
$h{ $child->name } = $value;
}
return \%h;
}
open my $fh, '<', 'database.kdl' or die "database.kdl: $!";
my $doc = parse_kdl($fh);
my $config = node_to_hash($doc->nodes->[0]);
For this file:
database {
host "db.example.com"
port 5432
replica "r1.example.com" "r2.example.com"
options ssl=#true timeout=30
}
$config is:
{
host => 'db.example.com',
port => 5432,
replica => [ 'r1.example.com', 'r2.example.com' ],
options => { args => [], ssl => 1, timeout => 30 },
}
Modify a document and write it back
Nodes and values are plain blessed hashes; edit them in place and emit.
use Text::KDL::XS qw(parse_kdl emit_kdl);
my $doc = parse_kdl("server \"a\" port=80\nserver \"b\" port=80\n");
for my $node (@{ $doc->nodes }) {
next unless $node->name eq 'server';
$node->prop('port')->{value} = 8080;
push @{ $node->args }, Text::KDL::XS::Value->new(type => 'string', value => 'edited');
}
print emit_kdl($doc);
# server a edited port=8080
# server b edited port=8080
Changing the value inside an existing property, as above, is fine as long as it still fits the value's type and kind (emit_kdl checks it again). Adding, removing or reordering entries of $node->props is fine too: $node->prop searches the array each time, so after push @{ $node->props }, [ port => $v ] the call prop('port') returns the new value.
Comments and formatting of the original are not preserved; the output is re-generated from the tree (see "What a round trip loses").
Convert KDL to JSON and back
use JSON::PP;
use Text::KDL::XS qw(parse_kdl emit_kdl);
my $json = JSON::PP->new->canonical->pretty;
# KDL -> JSON: as_data keeps names, arguments, properties and children
# (but not value annotations or the number kind).
my $doc = parse_kdl(<<'KDL');
person name="Kat" age=42 {
hobby "kdl"
hobby "perl"
}
KDL
print $json->encode($doc->as_data);
# JSON -> KDL: data mode maps objects to children and arrays to arguments.
my $data = $json->decode('{"name":"Kat","langs":["perl","c"],"address":{"city":"Berlin"}}');
print emit_kdl($data);
# address {
# city Berlin
# }
# langs perl c
# name Kat
JSON booleans decode to JSON::PP::Boolean objects, which emit_kdl recognises and writes as #true/#false.
Stream a large file without building a tree
Text::KDL::XS::Parser yields one event at a time and never builds the document tree. With a filehandle or code reference source it reads the input in chunks, so memory use depends on the nesting depth and the size of the current event, not on the length of the document (a string source is copied once in full). Give it a filehandle:
use Text::KDL::XS::Parser;
open my $fh, '<:raw', 'huge.kdl' or die $!;
my $p = Text::KDL::XS::Parser->new($fh);
my ($depth, %count) = (0);
while (my $ev = $p->next_event) {
if ($ev->{event} eq 'start_node') {
$count{ $ev->{name} }++ if $depth == 0; # count top-level nodes by name
$depth++;
}
elsif ($ev->{event} eq 'end_node') {
$depth--;
}
}
Events for arguments and properties carry a Text::KDL::XS::Value in $ev->{value}; the event hash is described in "EVENT HASH" in Text::KDL::XS::Parser.
Read from STDIN, a socket or a pipe
Any filehandle works, with or without an encoding layer, even after some lines have been read from it:
my $doc = parse_kdl(\*STDIN); # or parse_kdl(*STDIN)
Filehandles are read with Perl's read, which waits until it has a full chunk (a few kilobytes) or the input ends. That is ideal for files; on a slow pipe or socket it means the first events can arrive later than the data they describe. For low latency with Text::KDL::XS::Parser, read with sysread in a code reference, which returns whatever has arrived:
binmode STDIN; # sysread needs a handle without an encoding layer
my $parser = Text::KDL::XS::Parser->new(sub {
my ($wanted_bytes) = @_;
my $count = sysread STDIN, my $chunk, $wanted_bytes;
die "cannot read STDIN: $!" unless defined $count;
return $chunk; # '' at end of input
});
If the data arrives in chunks from somewhere else (an HTTP body, a decompressor), use a code reference as well. It is called with the number of bytes the parser would like; that is only a hint, a chunk of any length is accepted. Return an empty string or undef at end of input; after that the code reference is not called again:
my @pending = (join('', map { "n$_ 1\n" } 1 .. 2000), ''); # one big chunk, then the end
my $doc = parse_kdl(sub { shift @pending });
print scalar @{ $doc->nodes }; # 2000
Chunks are UTF-8 bytes (a character string is encoded for you), and their boundaries do not need to align with lines, tokens or even characters. An exception thrown inside the code reference propagates out of parse_kdl (or next_event) unchanged, so a die with an error object arrives as that object.
Handle parse errors
parse_kdl and next_event die on malformed input with the reason given by the underlying library, reported at the line of your call:
KDL parse error: Unexpected end of data (unclosed lists of children) at load.pl line 12.
(Perl appends , <$fh> line N. if a filehandle was read with <> earlier in the program.) The message carries no line number of the input; the underlying library does not track positions.
my $doc = eval { parse_kdl($text) };
if (!$doc) {
my $err = $@;
die "config.kdl is not valid KDL: $err";
}
A streaming parser that has failed stays failed: every further next_event dies with the same error. emit_kdl dies for values it cannot serialise and for bad options, again at your line:
emit_kdl: cannot serialize CODE ref at save.pl line 7.
emit_kdl: unknown version 'v3' (expected 'detect', '1' or '2') at save.pl line 9.
"ERRORS" in Text::KDL::XS lists every message.
Validate against an expected structure
KDL has no schema language built in. A few lines of Perl over the tree usually suffice:
my $doc = parse_kdl($text);
my %required = (host => 'string', port => 'number');
my ($node) = grep { $_->name eq 'database' } @{ $doc->nodes };
die "missing database node" unless $node;
for my $key (sort keys %required) {
my $v = $node->prop($key);
die "database: missing $key" unless $v;
die "database: $key must be $required{$key}" unless $v->type eq $required{$key};
}
Parse untrusted input
The parser is bounded by the size of the input, and nesting is bounded by the max_depth option: by default a document nested more than 512 levels deep dies with KDL parse error: nesting depth exceeds max_depth (512) before the tree gets any deeper. That keeps walking, emitting or as_data-ing the tree cheap. Lower the limit to what your format needs:
my $doc = eval { parse_kdl($untrusted, max_depth => 16) }
or die "rejected: $@";
Everything else is checked as well: the input must be valid UTF-8, and the parser rejects every document that is not valid KDL. What remains your job is the size of the input (read at most as many bytes as you are willing to hold) and the meaning of the data (see "Validate against an expected structure").
SEE ALSO
Text::KDL::XS for the API reference, Text::KDL::XS::Parser for the streaming interface, Text::KDL::XS::Value for the value model.
The KDL specification: https://kdl.dev and https://github.com/kdl-org/kdl.
AUTHOR
Davenonymous <perl@davenonymous.com>
LICENSE
Copyright (C) 2026 Davenonymous.
This document is part of the Text-KDL-XS distribution and is released under the same terms as Perl itself.