Skip to content

Round-tripping strips quotes, so "true", "1.0", "007" and "null" come back as bool/number/null, and empty values become '' #137

Description

@matt-edmondson

What's wrong

YamlSerializer loses scalar types in both directions:

  • Read. Line 71 deserializes into Dictionary<object, object>. Every scalar comes back as a string, and the reader no longer knows whether it was quoted.
  • Write. The serializer (lines 35–38) is built without WithQuotingNecessaryStrings(), so strings that look like other types are written bare.
  • Nulls. ConvertValue (line 156) maps null => string.Empty, so empty: or empty: ~ is written as empty: ''.

Reproduction

Run against the current main:

Frontmatter.CombineFrontmatter(
    "---\ntitle: \"true\"\nversion: \"1.0\"\nzip: \"01234\"\nnull_str: \"null\"\nempty:\ndraft: false\n---\nbody\n",
    FrontmatterNaming.AsIs, FrontmatterOrder.AsIs, FrontmatterMergeStrategy.None);

Output:

title: true
version: 1.0
zip: 01234
null_str: null
empty: ''
draft: false

The public API writes the same way:

Frontmatter.AddFrontmatter("body", new Dictionary<string, object> { ["title"] = "true", ["id"] = "007" });
// ---
// title: true
// id: 007
// ---

Why it matters

The output is read by other YAML consumers: Jekyll, Hugo, Astro, js-yaml, PyYAML and others. After a pass through this library:

  • title: "true" becomes the boolean true
  • version: "1.0" becomes the float 1
  • zip: "01234" becomes an integer, octal in YAML 1.1 parsers, which drops the leading zero
  • null_str: "null" becomes a real null
  • an intentionally null field becomes an empty string

A formatter that only reorders or renames properties should never change what they mean.

Suggested fix / acceptance criteria

Adding WithQuotingNecessaryStrings() on its own is not enough. Every scalar is currently a string, so it would also start quoting the genuine draft: false as 'false'. Both halves are needed:

  1. Keep scalar types on read, so plain scalars become bool/int/float and quoted scalars stay strings. YamlDotNet 18.x has DeserializerBuilder.WithAttemptingUnquotedStringTypeDeserialization() for this.
  2. Add WithQuotingNecessaryStrings() to the serializer.
  3. Map null to null instead of string.Empty. The serializer already uses DefaultValuesHandling.Preserve, so it will write the null.

Tests: running the input above through CombineFrontmatter should produce output that parses to the same types and values as the input, with draft still an unquoted boolean.

Activity

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

Metadata

Metadata

Labels

readyFully specified; implement as written

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions