Repository navigation
Wrapper set arities read JSON element tokens their primitive set refuses (quoted, padded, signed numbers; braced and unhyphenated Guids) #141
Description
Found by SetWrapperParityTests (#139), the sibling parity sweep between a primitive set and its wrapper arity over an identity element.
Problem
docs/serialization.md says that for the non-temporal families the JSON is "byte for byte the same: Int32Set<OrderId> and Int32Set are indistinguishable". On write that holds. On read the wrapper arity accepts element tokens its primitive sibling refuses, because the wrapper's element converters (ValueSetIntegerElementJsonConverter, ValueSetTextElementJsonConverter) hand the token's text to the family's array-literal grammar, while the primitive set reads its elements with System.Text.Json's own converter.
Measured with the default JsonSerializerOptions (SmallCode/TenantId are the conventions project's representative wrappers; an identity wrapper behaves the same):
| JSON | Int32Set |
Int32Set<SmallCode> |
|---|---|---|
["7"] |
JsonException |
{7} |
[" 7 "] |
JsonException |
{7} |
["+7"] |
JsonException |
{7} |
| JSON | GuidSet |
GuidSet<TenantId> |
|---|---|---|
[" 6f9619ff-8b86-d011-b42d-00c04fc964ff "] |
JsonException |
{6f9619ff-…} |
["{6f9619ff-8b86-d011-b42d-00c04fc964ff}"] |
JsonException |
{6f9619ff-…} |
["6f9619ff8b86d011b42d00c04fc964ff"] |
JsonException |
{6f9619ff-…} |
The same holds for Int16Set<T>, Int64Set<T> and DecimalSet<T>. Under NumberHandling.AllowReadingFromString (ASP.NET Core's web defaults) ["7"] agrees, but [" 7 "] still reads only through the wrapper.
The converter's own remarks say "Reads accept a JSON string unconditionally". That is the intent for AllowReadingFromString, but it ignores the options, and the literal grammar is wider than System.Text.Json's (whitespace, +, the other uuid_in spellings). The temporal and NodaTime arities agree with their primitive, because those primitive sets already read their JSON elements through the same grammar.
Expected
Swapping Int32Set for Int32Set<OrderId> doesn't change which payloads an endpoint accepts. The wrapper's element reader accepts a string token only when the options allow reading numbers from strings, and it reads the token with System.Text.Json's grammar for the primitive (or the primitive set's reader), not the array-literal one.
Notes
- It isn't a wrong value: every accepted payload reads as the number it spells. It's a widening, and a payload that one sibling accepts and the other refuses.
- Tightening it refuses input that is accepted today, so it's
breaking. - The candidate rows are in
test/oracles/candidates.jsonunderSetWrapperParityTests. The fix's PR removes them.
Activity
CaffeinatedCoder commentedon Oct 18, 2026
Fixed in #164 (stacked on #163): the integer and decimal wrapper converters read a string token only under
JsonNumberHandling.AllowReadingFromString, with the grammar System.Text.Json reads inside the quotes (verified againstUtf8Parser: sign and digits; sign, digits, point and exponent; never whitespace), and a newValueSetGuidElementJsonConverterreads a wrapper Guid withUtf8JsonReader.GetGuid— the hyphenated form only.WriteAsStringalone no longer round-trips a wrapper set, as it never did a plain one. The whitespace row forGuidSetwent with #162 already (theuuid_ingrammar refuses it too); the four numeric rows go with this PR. The value-semantics review added one more commit: the Guid converter now also refuses, at the write, an element that formats without hyphens, since neither sibling could read that payload back. Proven by reverting the fix.