Skip to content

Fix block scalar indentation indicator - #393

Open
MsfPablo wants to merge 2 commits into
yaml:mainfrom
MsfPablo:fix-block-scalar-indent-indicator
Open

MsfPablo wants to merge 2 commits into
yaml:mainfrom
MsfPablo:fix-block-scalar-indent-indicator

Conversation

@MsfPablo

@MsfPablo MsfPablo commented Aug 12, 2026 •

Copy link
Copy Markdown

Problem

Reported against the archived v3 repo as go-yaml/yaml#1071, and still present in v4.

Marshalling a string whose first line starts with a space, inside a block sequence item, produces a document that cannot be parsed back:

type Params struct {
	Description string `yaml:"description"`
}
type Spec struct {
	Parameters []Params `yaml:"parameters"`
}

spec := Spec{Parameters: []Params{{Description: "  a\nb"}}}
out, _ := yaml.Marshal(&spec)
var got Spec
err := yaml.Unmarshal(out, &got)

Before this change out is:

parameters:
    - description: |4-
          a
        b

and Unmarshal fails with did not find expected key.

Cause

writeBlockScalarHints always emitted BestIndent as the explicit indentation indicator. Per the YAML spec that indicator is the scalar's indentation relative to its parent node, so BestIndent is only right when the scalar is indented exactly BestIndent columns past its parent.

This emitter does not always do that: increaseIndent adds only 2 columns for the content of a block sequence item (the width of the - indicator). In the example above the scalar sits 2 columns past its parent, but the emitted indicator says 4, so the parser looks for content at the wrong column.

Fix

Derive the indicator from the actual difference between the scalar's indentation and its parent's indentation. The result is now:

parameters:
    - description: |2-
          a
        b

which round-trips. BestIndent is kept as a fallback if the difference ever falls outside the 1-9 range an indicator can express.

Tests

Added TestBlockScalarIndentIndicatorRoundTrip in yaml_test.go, covering several leading-space values across indent settings 0/1/2/3/4/8.

make test passes (ALL TESTS PASS; yaml-test-suite at PASS: 1383 / FAIL: 225 (known: 225), unchanged), as do make fmt and make lint (0 issues.).

Note

I found this issue while working through open reports as part of Anthropic's "Claude for Open Source" program, and used Claude Code while diagnosing and preparing the change. I have reviewed and verified everything here myself, and I am happy to iterate on the approach or the naming if you would prefer something different. If unsolicited AI-assisted contributions are not welcome here, please just close this and accept my apologies for the noise.


Maintainers' edits

Fixes #399

The explicit indentation indicator emitted for a block scalar whose
content starts with a space was always BestIndent.
That is only correct when the scalar is indented BestIndent columns
past its parent node, but this emitter indents a block sequence item's
content by just 2 columns.
In that case the emitted indicator was too large and the resulting
document failed to parse, or lost the scalar's leading spaces.

Compute the indicator from the actual difference between the scalar's
indentation and its parent's indentation instead.

Fixes go-yaml/yaml#1071
Comment thread yaml_test.go
Comment on lines +2742 to +2776
// A block scalar whose content starts with a space needs an explicit
// indentation indicator, and that indicator is relative to the indentation of
// the parent node.
// See https://github.com/go-yaml/yaml/issues/1071
func TestBlockScalarIndentIndicatorRoundTrip(t *testing.T) {
type params struct {
Description string `yaml:"description"`
}
type spec struct {
Parameters []params `yaml:"parameters"`
}

for _, indent := range []int{0, 1, 2, 3, 4, 8} {
for _, description := range []string{
" a\nb",
" a\nb",
" a\n b",
} {
in := spec{Parameters: []params{{Description: description}}}

var buf bytes.Buffer
enc := yaml.NewEncoder(&buf)
if indent > 0 {
enc.SetIndent(indent)
}
assert.NoError(t, enc.Encode(&in))
assert.NoError(t, enc.Close())

var out spec
err := yaml.Unmarshal(buf.Bytes(), &out)
assert.NoErrorf(t, err, "indent %d, encoded as:\n%s", indent, buf.String())
assert.DeepEqualf(t, in, out, "indent %d, encoded as:\n%s", indent, buf.String())
}
}
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yaml/go-yaml repository chose to use a yaml test suite.

I feel like your test can be added to emitter.yaml file with roundtrip feature.

https://github.com/yaml/go-yaml/blob/main/internal%2Flibyaml%2Ftestdata%2Femitter.yaml

You will find other examples in this file.

I would appreciate if you could try to move your test there.

Comment on lines +1995 to +1999
parent_indent := 0
if len(emitter.indents) > 0 && emitter.indents[len(emitter.indents)-1] > 0 {
parent_indent = emitter.indents[len(emitter.indents)-1]
}
indent := emitter.indent - parent_indent

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nitpicking: The fix makes sense, but the new indentation logic is a little hard to read at first glance. I think it would benefit from either a small helper or a brief explanation of why the indicator is based on the parent/current indent difference.

What do you think about the following suggestion ?

Suggested change
parent_indent := 0
if len(emitter.indents) > 0 && emitter.indents[len(emitter.indents)-1] > 0 {
parent_indent = emitter.indents[len(emitter.indents)-1]
}
indent := emitter.indent - parent_indent
indent := emitter.indent
if n := len(emitter.indents); n > 0 {
parentIndent := emitter.indents[n-1]
if parentIndent > 0 {
// Subtract the parent node's indent: the explicit indicator is
// relative to the parent, not absolute.
indent -= parentIndent
}
}

@ccoVeille

ccoVeille commented Aug 16, 2026 •

Copy link
Copy Markdown
Contributor

It sounds interesting to compare this fix to this one

They seem to address the same thing

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Marshal produces spec-invalid YAML for multi-line strings inside block sequences

2 participants