Skip to content

How a written value is spelt

Every write goes in as a Go value and comes out as YAML text. This page is the complete set of rules for what that text is. The contract behind all of them is one sentence: the value you write reads back as the same type and value under the document's own YAML version, and the source around it is not touched. Everything below is how the spelling is chosen to keep that true while looking like something a person would have written.

The rules are pinned by spelling_docs_test.go, which runs every row of the tables below; if the engine and this page disagree, the test fails.

Scalar kinds

Go value Written as Notes
string plain, double-quoted, or a literal block See Strings
int, int64, uint decimal: 42 Exact; never through a float
*big.Int beyond 64 bits its decimal digits: 1180591620717411303424 Exact
float32, float64 positional with a fractional part: 1.5, 3.0, 0.1 The shortest decimal that reads back bit-identical (D90)
a float past Go's %g thresholds signed exponent: 1.0e+21, 1.0e-5 Still a float under Core, JSON and YAML 1.1
bool true / false
nil null Over an empty value (k:) the value stays empty, which already means null
time.Time !<tag:yaml.org,2002:timestamp> 2001-01-02T03:04:05Z under YAML 1.2; 2001-01-02T03:04:05Z under YAML 1.1 Core has no implicit timestamps, so the tag is explicit and verbatim; YAML 1.1's schema recognises the form
[]byte !<tag:yaml.org,2002:binary> aGk= Base64, explicit tag in every profile

A type the profile cannot represent, such as a float under Failsafe, is refused with ErrUnsupportedType rather than written as text.

Strings

A string is written plain when plain is safe, double-quoted when it is not, and as a literal block when it holds line breaks and a block can go there.

Plain

The text is written bare when, under the document's version, it would read back as exactly that string and nothing else:

k: new
k: 😀
k: a    b          # a tab is fine in a YAML 1.2 plain scalar
k: yes         # a string under YAML 1.2, where yes is not a boolean

Double-quoted

Anything that would change meaning or shape as a plain scalar goes in double quotes, with escapes where needed:

Text Written Why
42, null, 1e3 "42", "null" Would read back as a number or null
yes under YAML 1.1 "yes" A boolean there
a: b, a # b "a: b", "a # b" Would become a mapping or a comment
"" "" An empty plain scalar is null
lead, trail " lead", "trail " Plain scalars lose edge whitespace
a b under YAML 1.1 "a\tb" Tabs are not plain there
a U+202E b (a bidi override or zero-width character) "a\u202eb" Escaped so a reader can see it (CVE-2021-42574)
l1\r\nl2 "l1\r\nl2" A carriage return has no block spelling

Single quotes are never chosen for a new value; they are kept where they already are (see Existing scalars).

Literal block

A string containing \n is written as a literal block scalar when the target is in block context: below a mapping key or on a sequence item's dash line, one indentation step in, using the document's own step and line ending.

Text Written Chomping
l1\nl2\n k: \|
l1
l2
clip: one trailing break
l1\nl2 k: \|-
l1
l2
strip: none
l1\n\n k: \|+
l1
(empty line)
keep: all of them
l1\nl2\n k: \|2
l1
l2
an indent indicator, because the first line begins with whitespace
p1\n\np2\n k: \|
p1
(empty line)
p2
interior blank lines are written bare, never as indentation

The block's final line break replaces the scalar's own, so the file gains no blank line. The body reads back exactly, including under YAML 1.1.

It stays double-quoted, with \n escapes, where a block cannot go:

Situation Written
inside a flow collection: {k: old} {k: "l1\nl2\n"}
as a mapping key "l1\nl2": v
a comment follows the scalar on its line: k: old # c k: "l1\nl2\n" # c
the scalar is the last thing in the file, with no line break after it k: "l1\nl2\n"
a line ends in a space or tab: l1 \nl2\n "l1 \nl2\n", since a block cannot tell that from layout
the text contains \r "l1\r\nl2"
the existing scalar is single-quoted see below

Existing scalars

Replacing a scalar keeps its spelling where the new value fits it. The rule is D60: the style is retained, no tag is added to keep it, and a style that cannot hold the value gives way.

Existing New string Written
plain k: old any as for a new value above
empty k: any as for a new value; nil leaves it empty
single-quoted k: 'old' one line k: 'new', k: 'yes', k: '42': single quotes hold anything but a break or a control character
single-quoted with line breaks k: 'l1
l2
': the quotes stay and each break is folded as a blank line, which reads back exactly
single-quoted with \r or an invisible character double-quoted; a tab is fine in single quotes under either version
double-quoted k: "old" one line k: "new", k: "😀": double quotes stay
double-quoted with line breaks a literal block, as for a new value
literal k: \| anything, one line or many the body is replaced and the block stays: k: \|-
new; the chomping indicator follows the new text's trailing breaks, and an indent indicator is added when its first line begins with whitespace
folded k: > anything as literal, with > kept: k: >-
new
any block with \r or an invisible character double-quoted

A non-string value written over a quoted or block scalar takes its own spelling (k: 'old'k: 42); a timestamp or binary keeps a quoted style for its content where there was one (!<…timestamp> '2001-01-02T03:04:05Z').

An existing scalar with an explicit tag keeps it when the new value is the same type (!!str old!!str 42: the tag already makes it a string), and refuses a value of a different type with ErrTagIntent. An anchored scalar keeps its anchor.

Mapping keys

A new key is written plain when its text reads back as the same string key, and double-quoted otherwise, so 09 or true as a key becomes "09": and "true": rather than an integer or boolean key. An existing key is never respelt; it is matched by value, so StringStep("port") finds port: and KeyStep(IntKey(8080)) finds 8080: or 0x1f90:.

Collections

Written into New collection
a block mapping or sequence block style, one indentation step in, the step inferred from the container's own nesting (two spaces when there is nothing to infer)
a block sequence, a mapping value compact: - key: value, later entries aligned under the first
a flow collection flow style: {a: 1}, [a, b]
an empty stream (CreateDocument) block style, two-space indentation, LF, a final newline; a --- only after a comment-only header
anywhere, when empty {} or []

A Go map has no order, so its keys are written sorted; MappingInput writes entries in the order given. A scalar with a comment on its line that becomes a collection is written in flow style on that line, so the comment keeps its owner.

Version differences

YAML 1.2 (Core) YAML 1.1
yes, no, on, off as strings plain double-quoted
a tab in a string plain double-quoted
time.Time explicit verbatim tag plain, the schema recognises it
floats 1.5, 1.0e+21 the same spelling: both grammars accept it

Document.Version reports which rules a document is under.