Skip to content

Support empty keys in block and flow mappings - #558

Open
sndth wants to merge 3 commits into
fktn-k:developfrom
sndth:fix/empty-keys
Open

Support empty keys in block and flow mappings#558
sndth wants to merge 3 commits into
fktn-k:developfrom
sndth:fix/empty-keys

Conversation

@sndth

@sndth sndth commented Aug 16, 2026

Copy link
Copy Markdown
Contributor

This PR fixes #557.

An empty key was only handled at the very start of a document, and it produced an entry whose key
is an empty string. Everywhere else the key separator was rejected, and inside a flow mapping it
was ignored, which dropped the entry:

key: value
: empty key                  # parse_error: Detected invalid indentation
- : foo                      # parse_error: mapping key should not be empty
{ key: value, : empty key }  # parse_error: The ":" mapping value indicator must be
                             # followed after a mapping key

The change

add_empty_key_entry() adds an entry with a null key and moves to the token after the separator.
It is called wherever a key separator turns up with no key before it: at the document root, at the
start of a line in a block mapping, in a block sequence entry, and in a flow mapping. The
deliberate mapping key should not be empty rejection for block sequence entries is gone.

The value of an empty key can be omitted too, so the helper also moves back to the parent mapping
when the following token is not indented deeper - the same rule a normal key already follows:

:
foo: bar
# -> {null: null, foo: bar}

:
  foo: bar
# -> {null: {foo: bar}}
input before after
": foo" {"": foo} {null: foo}
":" {"": null} {null: null}
"key: value\n: empty key" parse_error {key: value, null: 'empty key'}
":\nb: 1" parse_error {null: null, b: 1}
"- : foo" parse_error [{null: foo}]
"{key: value, : empty key}" parse_error {key: value, null: 'empty key'}
"\"\": foo" {"": foo} unchanged, a quoted key stays a string

Breaking change

As you noted in #553, an empty key is now a null node rather than an empty string, so code reading
such an entry as node[""] has to read node[nullptr] instead. Two existing subcases in
test_deserializer_class.cpp asserted the old behavior and are updated here. I did not touch any
version macro, assuming the v0.5.0 bump happens at release time as usual.


Pull Request Checklist

Read the CONTRIBUTING.md file for detailed information.

  • Changes are described in the pull request or in a referenced issue.
  • The test suite compiles and runs without any error.
  • The code coverage on your branch is 100%.
  • The documentation is updated if you added/changed a feature.

Please don't

  • The C++11 support varies between different compilers and versions. Please note the list of supported compilers. Some compilers like GCC 4.7 (and earlier), Clang 3.3 (and earlier), or Microsoft Visual Studio 13.0 and earlier are known not to work due to missing or incomplete C++11 support. Please refrain from proposing changes that work around these compiler's limitations with #ifdefs or other means.
  • Please refrain from proposing changes that would break YAML specifications. If you propose a conformant extension of YAML to be supported by the library, please motivate this extension.
  • Please do not open pull requests that address multiple issues.

@sndth
sndth requested a review from fktn-k as a code owner August 16, 2026 17:42
@coveralls

coveralls commented Aug 16, 2026

Copy link
Copy Markdown

Coverage Report for CI Build 32262347650

Coverage remained the same at 100.0%

Details

  • Coverage remained the same as the base build.
  • Patch coverage: 48 of 48 lines across 1 file are fully covered (100%).
  • No coverage regressions found.

Uncovered Changes

No uncovered changes found.

Coverage Regressions

No coverage regressions found.


Coverage Stats

Coverage Status
Relevant Lines: 5370
Covered Lines: 5370
Line Coverage: 100.0%
Relevant Branches: 1532
Covered Branches: 1532
Branch Coverage: 100.0%
Branches in Coverage %: Yes
Coverage Strength: 1717.33 hits per line

💛 - Coveralls

An empty key was a special case at the document root only, where it produced
an entry with an empty string key. Everywhere else a key separator without a
preceding key was rejected, and in a flow mapping it was ignored, which
dropped the entry:

```yaml
key: value
: empty key      # parse_error: Detected invalid indentation
- : foo          # parse_error: mapping key should not be empty
{ key: value, : empty key }
                 # parse_error: The ":" mapping value indicator must be
                 # followed after a mapping key
```

Parse an empty key as a null node, as the YAML specification does, and add the
entry wherever a key separator is found with no key before it: at the start of
a line in a block mapping, in a block sequence entry, and in a flow mapping.
The value of an empty key can be omitted as well, so add_empty_key_entry()
also moves back to the parent mapping when the following token is not indented
deeper, the same way an omitted value is handled for a normal key.

This changes the type of an empty key from an empty string to a null node,
which is a breaking change for anyone reading such an entry as node[""].
A quoted empty key ("": value) is unaffected and remains a string.

Spec example 8.18 (test suite case S3PD) and the empty key cases NKF9 and PW8X
of the yaml-test-suite now parse. CFD4 (an empty key in a single pair flow
sequence) is still rejected: it needs single pair flow mappings, which are not
supported for non-empty keys either ([foo: 1] is rejected as well).
@sndth

sndth commented Aug 18, 2026

Copy link
Copy Markdown
Contributor Author

I added the ? : patch here as a second commit.

The reasoning follows yours: ? introduces an explicit key, and a : on the same line belongs to
the contents of that key rather than to the entry the key is part of. So that separator now turns
the key node into a mapping and adds an entry with an empty key to it, exactly the way a scalar
does in ? foo: bar. Two follow-on adjustments were needed: the separator which later terminates
the explicit key has to find its context below the ones the key's own contents left on the stack,
and completing a pending explicit key at the end of a document has to unwind those contexts too.

input before after
? : {null: null} {{null: null}: null}
? : foo {foo: null} {{null: foo}: null}
? foo: bar {} {{foo: bar}: null}
? : / : baz parse_error {{null: null}: baz}

The last two rows come along for free, and I think they are worth pointing out: ? foo: bar was
silently dropping the entire entry, and ? : followed by : baz was rejected. Both reproduce on
develop without either commit of this PR, so they are pre-existing rather than something the
first commit introduced.

I'll open the two issues you suggested, for an omitted value in a flow mapping ({foo: }) and for
single pair mappings in a flow collection ([foo: 1]), and link them here.

Comment thread include/fkYAML/detail/input/deserializer.hpp Outdated
Comment thread include/fkYAML/detail/input/deserializer.hpp Outdated
The contents of an explicit key may themselves be a mapping entry with an
empty key, as pointed out in fktn-k#557: `? ` introduces an explicit key, and the
`:` which follows it on the same line belongs to that key rather than to the
entry the key is part of.

Handle a key separator which begins the contents of an explicit key by turning
the key node into a mapping and adding an entry with an empty key to it, the
same way a scalar key does. The key separator which later terminates the
explicit key can now find its context below the ones its own contents left on
the stack, and completing a pending explicit key at the end of a document
unwinds those contexts too.

| input | before | after |
|---|---|---|
| `? :` | `{null: null}` | `{{null: null}: null}` |
| `? : foo` | `{foo: null}` | `{{null: foo}: null}` |
| `? foo: bar` | `{}` | `{{foo: bar}: null}` |
| `? :` / `: baz` | parse_error | `{{null: null}: baz}` |

The third row was silently dropping the whole entry before, and the fourth was
rejected outright; both are the same shape as the case this issue is about.
Comment thread include/fkYAML/detail/input/deserializer.hpp
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.

Empty keys are only supported at the document root, and as an empty string instead of null

3 participants