What's wrong
MetadataFile.Deserialize<T> (SourceGeneratorToolkit/MetadataFile.cs:25-28, used at :119) deserializes with options that set only PropertyNameCaseInsensitive = true. Everything else stays at the System.Text.Json defaults, and two of those defaults hide authoring errors:
- Unmapped members are skipped. A key that matches no property, such as the typo
"thigns" for "things", is dropped and the property keeps its default value.
- Duplicate properties are allowed and the last one wins.
{ "things": [...], "things": [] } quietly deserializes to an empty list.
Neither case raises a diagnostic. That contradicts the contract stated in CLAUDE.md: a missing or malformed file always reports, because swallowing a JsonException means malformed metadata silently generates something wrong. It also contradicts the remarks on Deserialize<T>.
Consumers can't opt in from the generator side:
DeserializeOptions is private.
GeneratorBase<T>.Generate(SourceProductionContext, MetadataSet) is sealed.
Repro
Using the existing ThingsGenerator test fixture:
Harness.Run(new ThingsGenerator(), new Dictionary<string, string>
{
["things.json"] = "{ \"thigns\": [ {\"name\":\"Alpha\"} ] }",
});
The run produces 1 generated source (a header and namespace Generated; with no classes) and 0 diagnostics. { "things": [ {"name":"Alpha"} ], "things": [] } behaves the same way.
The loader is stricter about syntax than about content: a stray // comment in the same file does report TST002.
Why it matters
In a hand-edited metadata file, a key typo is the most likely authoring mistake. It turns into a generator that "had nothing to emit", which is exactly the silent failure the toolkit is built to prevent. The consumer then sees missing generated types somewhere far from the cause.
Suggested fix
Add these two settings to DeserializeOptions:
UnmappedMemberHandling = JsonUnmappedMemberHandling.Disallow,
AllowDuplicateProperties = false,
- Both are available in the System.Text.Json 10.x package already referenced for netstandard2.0.
- The resulting
JsonException is already caught and reported as MetadataParseFailed.
- The exception message names the key, for example "The JSON property 'thigns' could not be mapped to any .NET member contained in type 'ThingsMetadata'".
Compatibility: a metadata file that carries a "$schema" key, or any other key the model doesn't bind, would now fail. A consumer model can opt back out per type with [JsonUnmappedMemberHandling(JsonUnmappedMemberHandling.Skip)], which takes precedence over the options. Call this out in the changelog, or explicitly tolerate $schema.
Acceptance criteria
- An unknown key reports
MetadataParseFailed, and the message names the key.
- A duplicated key reports
MetadataParseFailed.
- A model annotated
[JsonUnmappedMemberHandling(JsonUnmappedMemberHandling.Skip)] still accepts extra keys.
- Tests cover all three cases through
GeneratorHarness overrides.
Verification
This was checked with a temporary MSTest against the current main:
- The typo case and the duplicate-key case each produced 1 source and 0 diagnostics.
- With the proposed options,
JsonSerializer.Deserialize<ThingsMetadata> threw JsonException for both inputs.
- A type-level
[JsonUnmappedMemberHandling(Skip)] still accepted $schema.
What's wrong
MetadataFile.Deserialize<T>(SourceGeneratorToolkit/MetadataFile.cs:25-28, used at:119) deserializes with options that set onlyPropertyNameCaseInsensitive = true. Everything else stays at the System.Text.Json defaults, and two of those defaults hide authoring errors:"thigns"for"things", is dropped and the property keeps its default value.{ "things": [...], "things": [] }quietly deserializes to an empty list.Neither case raises a diagnostic. That contradicts the contract stated in CLAUDE.md: a missing or malformed file always reports, because swallowing a
JsonExceptionmeans malformed metadata silently generates something wrong. It also contradicts the remarks onDeserialize<T>.Consumers can't opt in from the generator side:
DeserializeOptionsis private.GeneratorBase<T>.Generate(SourceProductionContext, MetadataSet)is sealed.Repro
Using the existing
ThingsGeneratortest fixture:The run produces 1 generated source (a header and
namespace Generated;with no classes) and 0 diagnostics.{ "things": [ {"name":"Alpha"} ], "things": [] }behaves the same way.The loader is stricter about syntax than about content: a stray
//comment in the same file does report TST002.Why it matters
In a hand-edited metadata file, a key typo is the most likely authoring mistake. It turns into a generator that "had nothing to emit", which is exactly the silent failure the toolkit is built to prevent. The consumer then sees missing generated types somewhere far from the cause.
Suggested fix
Add these two settings to
DeserializeOptions:JsonExceptionis already caught and reported asMetadataParseFailed.Compatibility: a metadata file that carries a
"$schema"key, or any other key the model doesn't bind, would now fail. A consumer model can opt back out per type with[JsonUnmappedMemberHandling(JsonUnmappedMemberHandling.Skip)], which takes precedence over the options. Call this out in the changelog, or explicitly tolerate$schema.Acceptance criteria
MetadataParseFailed, and the message names the key.MetadataParseFailed.[JsonUnmappedMemberHandling(JsonUnmappedMemberHandling.Skip)]still accepts extra keys.GeneratorHarnessoverrides.Verification
This was checked with a temporary MSTest against the current main:
JsonSerializer.Deserialize<ThingsMetadata>threwJsonExceptionfor both inputs.[JsonUnmappedMemberHandling(Skip)]still accepted$schema.