Dictionary-key JSON compatibility
Use this reference to distinguish ordinary text map keys from embedded JSON keys,
and read acceptance from usable lookup or successful writing. Go complex-key
implementation is Not implemented. These are source-distributed contract
observations included with v0.3.0, not Go codec, runtime or schema support,
a shared registry or an Underlying extension.
Authority and profile
Section titled “Authority and profile”Authority is Fundamentals source commit
d2accc4a79b6bcf2708213c97093ab5ba6c06381, executed with .NET/System.Text.Json
10.0.12 and typed JavaScript fields on Node v26.8.1/TypeScript 6.0.3:
- C# complex-key factory determines admission and serializes keys as compact embedded JSON text.
- Dictionary reflection recognizes IDictionary shapes.
- Typed JS map handling
uses actual field key/value generic metadata; Guid concepts also declare
static valueType = Guid. - ValueMap equality
compares identity first, then native
JSON.stringifyfor object keys.
The fixture inventory contains 132 stimuli, 151 C#, 186 JS and 16 C# cross-runtime observations, six explicit kind declarations, 11 factory-admission types and exact case IDs. Provenance records source hashes, runtime identities and extraction limits. The optional reproducer executes original sources; ordinary Go tests validate evidence with only the standard library.
C# uses case-sensitive camelCase properties, the default encoder and ComplexKey,
EnumerableConceptAs, ConceptAs factories in that order. This selected profile is
not Globals: Globals adds acronym-friendly naming, null omission and other
converters. Neither installed package equivalence, Arc/Chronicle consumer profiles,
AOT nor trimming is demonstrated. A historical four-file match against main
9dbec5584351fc75dca31fe83443670746bb6e43 is source-only, not latest-package proof.
Admission and property-name layers
Section titled “Admission and property-name layers”The C# factory admits IDictionary with complex key types, including the captured string/Guid concept record classes and composite records. It excludes primitive, enum, string, Guid, decimal, Uri and built-in date/time key types. Ordinary string and Guid dictionaries use System.Text.Json’s built-in paths instead.
IReadOnlyDictionary<K,V> itself and Hashtable are not admitted. A concrete class
that also implements IDictionary can qualify even if it implements a read-only
interface. Admission does not prove construction of every custom dictionary.
A concept key plain serializes first to the standalone JSON value "plain";
that entire text becomes the outer object property name. Exact C# writes:
{"plain":42}{"\u0022plain\u0022":42}The first is an ordinary string dictionary; the second has a string-concept key.
After parsing only the outer object, their names are plain and "plain" (literal
quotes included). Go’s ordinary encoding.TextMarshaler map keys establish the
first contract, not this embedded-JSON contract. Existing scalar/concept
text-key coverage does not implement this factory.
Keys always use compact cloned options, even in the indented outer-write witness; values use the outer options. C# default escaping and JS native escaping differ at both layers. Captured quotes, backslashes, Unicode/astral text and HTML-sensitive characters interoperate for selected concept/composite declarations without raw write-string equality. Preserve raw duplicate properties, escaping and embedded whitespace; neither one JSON parse nor normalized JSON is a universal equality rule for keys.
Equality, ordering and last wins
Section titled “Equality, ordering and last wins”| Surface | Captured behavior |
|---|---|
| C# record keys | Dictionary record equality; duplicate decoded keys overwrite with the last value |
| C# input duplicate outer properties | JsonDocument enumerates both; indexer assignment overwrites |
| JS exact duplicate outer properties | JSON.parse collapses them before map traversal; last value survives |
| JS plain object keys with different insertion order | Native stringify differs; two ValueMap entries remain |
| JS actual typed Composite field | Declared field order normalizes the two incoming property orders; one entry remains |
Alternate escape spellings, embedded whitespace and reordered composite members
collapse in the captured C# dictionaries. The JS typed Composite normalization
is not a rule for all object keys or all ValueMaps. No canonical sorting, custom
equals() invocation or C# record equality is implied by JS ValueMap.
Typed-field asymmetries
Section titled “Typed-field asymmetries”The containing C# and JS models both initialize maps; these results belong to those declarations, not every model or serializer profile.
| Input or operation | C# selected profile | Actual JS typed field |
|---|---|---|
| Missing map | Keeps initialized empty dictionary | Overwrites initializer with undefined; .get() throws |
| Null map | Null; rewrites null | Null; .get() and writing throw |
| Empty map | Empty dictionary | Empty ValueMap |
| Number 7, zero or false instead of map | Rejects | Empty ValueMap |
| Nonempty array/string instead of map | Rejects | Enumerates indices; outcome depends on key type |
| String in Int32/Number-valued map | Rejects | Preserves string despite Number declaration |
| Null in Int32/Number-valued map | Rejects | Reads null; writing throws |
| Null object-valued entry | Accepts and writes null | Typed object read throws |
| Composite missing members | Guid.Empty and null Name | Undefined fields; rewrites embedded key as {} |
| Composite explicit null Guid | Rejects | Stores null; writing throws |
String-concept embedded key 0 | UnsupportedConceptValueType | Concept contains number 0 |
| Malformed outer JSON | Rejects | Rejects |
| Good key then malformed embedded key | Rejects; no returned partial object | Rejects; no returned partial object |
Accepted read, returned object, decoded map entries, .get() and rewrite are
separate fields in the evidence. A declared field type is not strict validation.
The late-failure witnesses do not guarantee failure atomicity for custom converter
side effects, streaming writes or mutation of existing objects.
Original versus fixed lookup and the Guid bug
Section titled “Original versus fixed lookup and the Guid bug”Historical runtimeFieldAccess probes query one fixed key per kind. Their
explicit lookupKey/queryMode: fixed-key metadata identifies that query. An
undefined result for zero, escaped or empty keys does not establish corruption.
Those results remain unchanged in the fixture.
A separate originalKeyLookup uses the independently supplied key for successful
JS and C# writes. There are 34 executed original-key lookup results, with
explicit not-attempted reasons on stimuli and the one failed C#-write read. These
recover zero Guid-concept and escaped/empty key values; they do not reinterpret
the old fixed-key outcomes.
Bare Guid is a concrete exception, tracked in Fundamentals #1151:
- C# ordinary Guid dictionary → typed JS Guid field: original-key lookup yields 42.
- JS bare Guid write → C# ordinary Guid dictionary: read rejects with JsonException/FormatException.
- JS bare Guid write → its own typed field: read accepts but decoded key is
094c5802-91NaN-f09d-4db7-958d39fce089; original Guid lookup is undefined. JS serializes the Guid to quoted JSON text, then passes the quoted text directly to Guid.parse on read rather than parsing the embedded JSON string first. - Guid concepts with declared
valueType = Guidinteroperate for the valid and zero witnesses. That evidence does not promise universal Guid-concept round-trip.
Consumer boundary
Section titled “Consumer boundary”Copy licensed fixtures from a pinned, publicly available Fundamentals.Go source archive; retain its full commit and provenance. Fixture tests need no sibling checkout, .NET or Node. Do not claim an unpushed revision is distributable.
Arc owns map traversal, bindings, generated field plans and proxy/schema output.
Chronicle owns event/schema admission and its configured serialization profile.
Both must execute those paths before claiming consumer parity. This evidence
alone does not establish two Go consumers needing one shared key-codec helper.
No shared codec, general serializer, registry or Underlying change is introduced;
#18 remains the consumer-evidence
boundary.