Claude: changes for issue #49 #57

Merged
Grandiras merged 3 commits from claude/issue-49 into main 2026-07-25 02:27:51 +00:00
Collaborator

Requested in #49.

Requested in #49.
fix: collapse AtProtoRecord discriminator to a single $type (closes #49)
All checks were successful
CI / build-and-test (pull_request) Successful in 39s
a886cc3aa7
AtProtoRecord.Type is abstract and carries [JsonPropertyName("$type")] on
the base member. System.Text.Json neither inherits that attribute through
an override nor collapses the base member and the override into one
contract property, so a record declared the documented way

    public override string Type => "com.example.todo.item";

serialized as BOTH "$type" (from the base member) and a stray camelCased
"type" (from the override) — polluting records that other AT Protocol
apps read.

Add AtProtoJsonDefaults.ApplyRecordTypeDiscriminator, a JsonTypeInfo
contract modifier that collapses every property mapping to the
AtProtoRecord.Type virtual slot down to one, named "$type" and emitted
first. Wire it into AtProtoJsonDefaults.Options and
LexiconTypeRegistry.CreateOptions(), so RecordCollection<T> and
RepoClient writes are fixed with no consumer change. The member is
public so apps serializing records with hand-built JsonSerializerOptions
can add it to DefaultJsonTypeInfoResolver.Modifiers.

Matching on the virtual slot (getter GetBaseDefinition) rather than the
member name leaves an unrelated `new`-shadowing Type member alone, and
re-declaring [JsonPropertyName("$type")] on the override — the workaround
consumers adopted — stays safe instead of colliding with the base member.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Author
Collaborator

🤖 Reviewing this PR — follow along in the action run; the reply will link the full session transcript.

🤖 Reviewing this PR — follow along in [the action run](https://git.grandiras.net/Grandiras/claude-bot/actions/runs/40); the reply will link the full session transcript.
Owner

@claude-bot Please fix the changelog conflict.

@claude-bot Please fix the changelog conflict.
Author
Collaborator

🤖 On it — follow along in the action run; the reply will link the full session transcript.

🤖 On it — follow along in [the action run](https://git.grandiras.net/Grandiras/claude-bot/actions/runs/41); the reply will link the full session transcript.
Merge remote-tracking branch 'origin/main' into claude/issue-49
Some checks failed
CI / build-and-test (pull_request) Has been cancelled
4f3bf7f1a5
# Conflicts:
#	CHANGELOG.md
Author
Collaborator

Fixed — the branch is merged with main and pushed (claude/issue-49, merge commit 4f3bf7f).

What the conflict was: main had moved ahead with #55 (issue #46, removing the no-op UnionJsonConverterFactory), which added its own bullet at the top of CHANGELOG.md's ### Fixed section — exactly where this PR added the #49 bullet. A pure adjacent-add conflict, no semantic overlap.

Resolution: kept both bullets — the #49 discriminator fix first, then the #46 IAtProtoUnion fix — with the rest of the Fixed list unchanged.

The two source files both branches touch (AtProtoJsonDefaults.cs, LexiconTypeRegistry.cs) auto-merged cleanly and the result is coherent: main's removal of the union converter registration and this PR's ApplyRecordTypeDiscriminator modifier land side by side in both CreateOptions() paths.

Verified:

  • dotnet build -p:EnableSourceControlManagerQueries=false — 0 errors (had to install the .NET 10 SDK in this container first; warnings are all pre-existing CS1591/NU1902 noise)
  • dotnet test tests/ATProtoNet.Tests/ -p:EnableSourceControlManagerQueries=false1010 passed, 0 failed, covering both this PR's RecordTypeDiscriminatorTests and main's AtProtoUnionMarkerTests

The PR diff against main is now just the 6 intended files (+276/−3).


⏱ 1m 58s · 16 turns · tokens: 25.8k in / 3.6k out (+518.8k cache-read)
📜 Full session transcript

Fixed — the branch is merged with `main` and pushed (`claude/issue-49`, merge commit `4f3bf7f`). **What the conflict was:** `main` had moved ahead with #55 (issue #46, removing the no-op `UnionJsonConverterFactory`), which added its own bullet at the top of `CHANGELOG.md`'s `### Fixed` section — exactly where this PR added the #49 bullet. A pure adjacent-add conflict, no semantic overlap. **Resolution:** kept both bullets — the #49 discriminator fix first, then the #46 `IAtProtoUnion` fix — with the rest of the `Fixed` list unchanged. The two source files both branches touch (`AtProtoJsonDefaults.cs`, `LexiconTypeRegistry.cs`) auto-merged cleanly and the result is coherent: `main`'s removal of the union converter registration and this PR's `ApplyRecordTypeDiscriminator` modifier land side by side in both `CreateOptions()` paths. **Verified:** - `dotnet build -p:EnableSourceControlManagerQueries=false` — 0 errors (had to install the .NET 10 SDK in this container first; warnings are all pre-existing CS1591/NU1902 noise) - `dotnet test tests/ATProtoNet.Tests/ -p:EnableSourceControlManagerQueries=false` — **1010 passed, 0 failed**, covering both this PR's `RecordTypeDiscriminatorTests` and `main`'s `AtProtoUnionMarkerTests` The PR diff against `main` is now just the 6 intended files (+276/−3). --- ⏱ 1m 58s · 16 turns · tokens: 25.8k in / 3.6k out (+518.8k cache-read) 📜 [Full session transcript](https://git.grandiras.net/Grandiras/claude-bot/src/branch/claude-logs/transcripts/Grandiras/ATProto.NET/issue-57/run-41.md)
Merge branch 'main' into claude/issue-49
All checks were successful
CI / build-and-test (pull_request) Successful in 38s
Sync Closures to GitHub / sync-closure (pull_request) Successful in 6s
effe39b3c9
Grandiras deleted branch claude/issue-49 2026-07-25 02:27:51 +00:00
Grandiras referenced this pull request from a commit 2026-07-25 02:27:51 +00:00
Author
Collaborator

📝 Retroactive review summary (was stuck as an invisible draft by the APPROVE/APPROVED bug, fixed in v12.2 — verdict was: approve):

Solid, well-scoped fix for the stray type/$type duplication caused by System.Text.Json not inheriting [JsonPropertyName] through property overrides. The ApplyRecordTypeDiscriminator contract modifier correctly scopes itself to AtProtoRecord-derived JsonTypeInfoKind.Object contracts, and IsTypeDiscriminator identifies the discriminator by resolving the virtual slot (GetBaseDefinition().DeclaringType == typeof(AtProtoRecord)) rather than by name, which correctly avoids false positives on an unrelated Type/new-shadowed member (covered by the LabelledItem test) while still collapsing the base member and the override (covered by TodoItem) as well as the pre-existing workaround of re-declaring [JsonPropertyName("$type")] on the override (covered by AnnotatedItem, which asserts no collision with the base member).

Good catches:

  • LexiconTypeRegistry.CreateOptions() was also updated to include the new modifier alongside ApplyUnionVariants, so the fix applies uniformly across both serializer-options construction paths (verified by Serialize_ThroughTypeRegistryOptions_WritesDollarTypeAndNoStrayType).
  • The pre-existing RecordCollectionTests.AtProtoRecord_SerializesWithDollarType test was fixed to actually serialize with AtProtoJsonDefaults.Options instead of the process-wide default options — previously it wasn't exercising the SDK's configured serializer at all, so it couldn't have caught this bug.
  • CHANGELOG.md, XML docs, and docs/custom-records.md are all updated in the same commit per repo convention, and clearly explain the STJ quirk plus the escape hatch for hand-built JsonSerializerOptions.

The new test suite (RecordTypeDiscriminatorTests) is thorough — covers plain overrides, the boxed-object serialization path actually used by CreateRecordAsync/PutRecordAsync, property ordering ($type first), deserialization round-trip, and the standalone-modifier usage pattern. I did not execute dotnet test in this sandbox (bash access was restricted), but the diff is internally consistent and the reasoning about GetBaseDefinition() correctly identifying the shared virtual slot (so both properties always report the same value, making the "keep first, discard rest" collapse safe) holds up under inspection. No correctness, security, or omission concerns found.


⏱ 1m 16s · 5 turns · tokens: 19.9k in / 6.3k out (+218.8k cache-read)
📜 Full session transcript

📝 **Retroactive review summary** (was stuck as an invisible draft by the APPROVE/APPROVED bug, fixed in v12.2 — verdict was: approve): Solid, well-scoped fix for the stray `type`/`$type` duplication caused by System.Text.Json not inheriting `[JsonPropertyName]` through property overrides. The `ApplyRecordTypeDiscriminator` contract modifier correctly scopes itself to `AtProtoRecord`-derived `JsonTypeInfoKind.Object` contracts, and `IsTypeDiscriminator` identifies the discriminator by resolving the virtual slot (`GetBaseDefinition().DeclaringType == typeof(AtProtoRecord)`) rather than by name, which correctly avoids false positives on an unrelated `Type`/`new`-shadowed member (covered by the `LabelledItem` test) while still collapsing the base member and the override (covered by `TodoItem`) as well as the pre-existing workaround of re-declaring `[JsonPropertyName("$type")]` on the override (covered by `AnnotatedItem`, which asserts no collision with the base member). Good catches: - `LexiconTypeRegistry.CreateOptions()` was also updated to include the new modifier alongside `ApplyUnionVariants`, so the fix applies uniformly across both serializer-options construction paths (verified by `Serialize_ThroughTypeRegistryOptions_WritesDollarTypeAndNoStrayType`). - The pre-existing `RecordCollectionTests.AtProtoRecord_SerializesWithDollarType` test was fixed to actually serialize with `AtProtoJsonDefaults.Options` instead of the process-wide default options — previously it wasn't exercising the SDK's configured serializer at all, so it couldn't have caught this bug. - CHANGELOG.md, XML docs, and `docs/custom-records.md` are all updated in the same commit per repo convention, and clearly explain the STJ quirk plus the escape hatch for hand-built `JsonSerializerOptions`. The new test suite (`RecordTypeDiscriminatorTests`) is thorough — covers plain overrides, the boxed-`object` serialization path actually used by `CreateRecordAsync`/`PutRecordAsync`, property ordering (`$type` first), deserialization round-trip, and the standalone-modifier usage pattern. I did not execute `dotnet test` in this sandbox (bash access was restricted), but the diff is internally consistent and the reasoning about `GetBaseDefinition()` correctly identifying the shared virtual slot (so both properties always report the same value, making the "keep first, discard rest" collapse safe) holds up under inspection. No correctness, security, or omission concerns found. --- ⏱ 1m 16s · 5 turns · tokens: 19.9k in / 6.3k out (+218.8k cache-read) 📜 [Full session transcript](https://git.grandiras.net/Grandiras/claude-bot/src/branch/claude-logs/transcripts/Grandiras/ATProto.NET/pr-57/run-40.md) <!-- claude-reviewed-head:a886cc3aa7fddf040363c55b02beed8cf82a03aa -->
Sign in to join this conversation.
No description provided.