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:
- 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.
- Add
WithQuotingNecessaryStrings() to the serializer.
- 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.
What's wrong
YamlSerializerloses scalar types in both directions:Dictionary<object, object>. Every scalar comes back as astring, and the reader no longer knows whether it was quoted.WithQuotingNecessaryStrings(), so strings that look like other types are written bare.ConvertValue(line 156) mapsnull => string.Empty, soempty:orempty: ~is written asempty: ''.Reproduction
Run against the current
main:Output:
The public API writes the same way:
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 booleantrueversion: "1.0"becomes the float1zip: "01234"becomes an integer, octal in YAML 1.1 parsers, which drops the leading zeronull_str: "null"becomes a real nullA 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 astring, so it would also start quoting the genuinedraft: falseas'false'. Both halves are needed:DeserializerBuilder.WithAttemptingUnquotedStringTypeDeserialization()for this.WithQuotingNecessaryStrings()to the serializer.nulltonullinstead ofstring.Empty. The serializer already usesDefaultValuesHandling.Preserve, so it will write the null.Tests: running the input above through
CombineFrontmattershould produce output that parses to the same types and values as the input, withdraftstill an unquoted boolean.