Skip to content

Serialize types implementing only IReadOnlyDictionary<,> as mappings - #1127

Open
youdie006 wants to merge 1 commit into
aaubry:masterfrom
youdie006:fix/readonlydictionary-serialization
Open

Serialize types implementing only IReadOnlyDictionary<,> as mappings#1127
youdie006 wants to merge 1 commit into
aaubry:masterfrom
youdie006:fix/readonlydictionary-serialization

Conversation

@youdie006

Copy link
Copy Markdown

Motivation

Fixes #606. A type that implements only IReadOnlyDictionary<TKey, TValue> (and neither the non-generic IDictionary nor IDictionary<TKey, TValue>) is serialized as a sequence of { Key, Value } mappings instead of a single YAML mapping:

# actual (before)
- Key: key1
  Value: value1
- Key: key2
  Value: value2

# expected
key1: value1
key2: value2

FullObjectGraphTraversalStrategy.TraverseObject dispatches dictionaries via ObjectFactory.GetDictionary, which only recognises IDictionary<,>. A read-only-only dictionary therefore returns false there and falls through to the IEnumerable branch (IReadOnlyDictionary<,> is IEnumerable<KeyValuePair<,>>), so it is written as a list. The BCL ReadOnlyDictionary<,> happens to also implement IDictionary, so only custom read-only dictionary types are affected. The asymmetry is real: the deserialize side already handles non-IDictionary dictionary types (see the existing GenericDictionaryThatDoesNotImplementIDictionaryCanBeDeserialized test).

Solution

  • ObjectFactoryBase.GetDictionary now also detects IReadOnlyDictionary<,> (after the existing IDictionary<,> check, so types implementing both keep the current path) and wraps the value in a new GenericReadOnlyDictionaryToNonGenericAdapter<TKey, TValue>.
  • The new adapter mirrors the existing GenericDictionaryToNonGenericAdapter but takes an IReadOnlyDictionary<,>. Object-graph traversal (TraverseDictionary) only enumerates the adapter, so only GetEnumerator is implemented; the rest throw NotSupportedException, exactly like the existing adapter's unused members.

The regular IDictionary<,> path is unchanged.

Test

Added SerializeGenericReadOnlyDictionary, plus a GenericTestReadOnlyDictionary<,> test helper (implements only IReadOnlyDictionary<,>, mirroring the existing GenericTestDictionary). It asserts the read-only dictionary serializes to the same YAML as an equivalent Dictionary<,>.

  • Before the fix: fails (serializes as a sequence).
  • After the fix: passes.

Verified locally in Docker: dotnet test — the full YamlDotNet.Test suite passes (the only 2 failures, ConformsWithYamlSpec, are pre-existing and caused by the YAML spec test-suite submodule not being present in the shallow checkout; they fail identically without this change). The library also builds cleanly for netstandard2.0 and net8.0 (0 warnings).


Disclosure: this fix was prepared with AI assistance (Claude). I reviewed it, confirmed the root cause in the traversal dispatch, and verified the red-green test and full suite myself.

A type that implements IReadOnlyDictionary<TKey, TValue> but neither the
non-generic IDictionary nor IDictionary<TKey, TValue> was serialized as a
sequence of {Key, Value} mappings instead of a single YAML mapping,
because ObjectFactoryBase.GetDictionary only recognised IDictionary<,>
and the object graph traversal then fell through to the IEnumerable
(list) branch.

Detect IReadOnlyDictionary<,> in GetDictionary and wrap it in a new
GenericReadOnlyDictionaryToNonGenericAdapter (mirroring the existing
IDictionary<,> adapter; only enumeration is needed for traversal) so it
is traversed as a mapping. The regular IDictionary<,> path is unchanged.

Fixes aaubry#606
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.

Classes extending IReadOnlyDictionary are not serialized as a yaml mapping

1 participant