Skip to content

TOML serializer silently loses Guid, decimal, TimeSpan and Uri properties, and throws on char instead of returning false #165

Closed

Description

@matt-edmondson

What's wrong

TomlSerializationProvider hands the model to Tomlyn 0.19.0 with only IgnoreMissingProperties = true:

  • the options: Essentials.SerializationProviders.Toml/TomlSerializationProvider.cs:29
  • the write: Toml.FromModel(obj, ModelOptions) at :58

There are no ConvertToToml / ConvertToModel hooks. Tomlyn's model writer does not treat several common .NET scalars as TOML values:

1. Guid, decimal, TimeSpan and Uri come back zeroed, and no error is raised. A model { Name="n", Id=<guid>, Price=12.34m, Timeout=30s, When=<DateTimeOffset>, Kind=K.B, U=new Uri("https://x.y/") } serializes to:

name = "n"
when = 2026-01-02T03:04:05Z
kind = "B"
[id]
[price]
[timeout]
[u]
segments = ["/"]

TrySerialize returns true. Reading it back gives Id = Guid.Empty, Price = 0, Timeout = 00:00:00, U = null.

The provider's own IsScalar (:219-228) lists Guid, decimal and TimeSpan as scalars, so it assumes Tomlyn writes them as values. It does not.

2. A char property makes TrySerialize throw. Tomlyn throws InvalidOperationException: Invalid primitive System.Char from ModelToTomlTransform.WritePrimitive. The catch list at :62-73 only covers TomlException, IOException and ObjectDisposedException. So:

  • TrySerialize throws instead of returning false, which breaks the contract in Essentials/ISerializationProvider.cs:29-35.
  • TrySerializeAsync faults.

b283163 fixed the same gap for System.Text.Json ("Report System.Text.Json's InvalidOperationException as a failed serialize").

Why it matters

Any persistence provider configured with TOML (FileSystem, Temp, ConfigHome, DataHome over Shared/FileRecordStore.cs) does this:

  1. It stores a record with a Guid id or a decimal amount and reports success.
  2. RetrieveAsync later returns a non-null object with the fields reset to their defaults.
  3. Because the object is non-null, the null guard does not fire.

The data is lost silently. Switching the serializer from JSON to TOML changes behaviour with no error.

The round-trip model in Essentials.Tests/SerializationRoundTripTests.cs:74-89 only has a string, an int, a list and a nested object, so nothing catches this.

This was reproduced against Tomlyn 0.19.0 with the provider's exact options at HEAD 7e2b3e2.

Suggested fix

  • Add ConvertToToml / ConvertToModel to ModelOptions so that:
    • Guid, TimeSpan, Uri and char are written as strings and parsed back;
    • decimal is written as a string, or as a TOML float when lossless, and parsed back.
  • Add catch (InvalidOperationException) to TrySerialize, and consider TargetInvocationException for throwing getters, so any type Tomlyn still rejects reports false rather than throwing.

Acceptance criteria

  • A shared round-trip test over a model with Guid, decimal, TimeSpan, Uri, char, DateTimeOffset and an enum gives equal values for every serialization provider, TOML included.
  • TrySerialize / TrySerializeAsync never throw for a type Tomlyn cannot represent. They return false.

Related: #69 (closed) fixed different TOML round-trip defects (multi-word names, lists, nested objects, records).

Activity

  1. matt-edmondson commented on Oct 29, 2026

    @matt-edmondson
    ContributorAuthor

    Triage


    Generated by Claude Code

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

Metadata

Metadata

Labels

bugSomething isn't working

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions