Latent #852 twin: UserProfile.Images uses Newtonsoft [JsonProperty] on a System.Text.Json column #854

Open
opened 2026-07-26 00:29:42 +00:00 by spikerj · 1 comment
Owner

Found while scoping #853. Not currently broken — filing so it doesn't become the next #852.

The hazard

UserProfile.Images is a List<ProfileImage> persisted as an EF JSON-string column via System.Text.Json (SpikerSoft.Data/Contexts/SpikerDbContext.cs:1059).

But ProfileImage's storage-key properties are annotated with Newtonsoft [JsonProperty]:

  • SpikerSoft.Data/Mongos/ProfileSettingsSubDocuments.cs:10[JsonProperty("gridFsFileId")] on GridFsFileId
  • SpikerSoft.Data/Mongos/ProfileSettingsSubDocuments.cs:31[JsonProperty("thumbnailGridFsId")] on ThumbnailGridFsId

System.Text.Json ignores [JsonProperty]. So these persist under their CLR names — PascalCase "GridFsFileId" / "ThumbnailGridFsId" — not the lowercase the attribute implies. The attribute is decorative and actively misleading.

Unlike ArtAsset.Artifacts, this converter has no case-insensitive read option, so there is no safety net.

Why it matters

This is #852's exact failure mode, unexploded. It is dormant purely because nobody has renamed those CLR properties. The moment someone does — exactly the "harmless rename" #849 looked like — every profile image key deserializes to its default and avatars/photos break, silently, with every sibling field still binding correctly.

ProfileTravelSubDocuments.cs:38 ([JsonProperty("gridFsId")] on GridFsId) is in the same JSON-column situation.

Note

These are genuinely still GridFS — Profile/avatars have not migrated to MinIO — so this is not a rename-to-objectKey task. The fix is to make the persisted contract honest and enforced:

  • Replace the Newtonsoft [JsonProperty] attributes with [JsonPropertyName] matching the name actually on disk today (PascalCase), or migrate the documents and pin the new name — the former is far cheaper and non-breaking.
  • Consider a guard test in the spirit of ArtStudioStorageKeyNamingTests pinning the serialized names.
  • Audit the other 63 HasConversion JSON columns for the same Newtonsoft-attribute-on-an-STJ-column pattern.

Related

  • #852 — the outage this pattern caused in Art Studio
  • #853 — the proper rename that removes the shim there
Found while scoping #853. **Not currently broken** — filing so it doesn't become the next #852. ## The hazard `UserProfile.Images` is a `List<ProfileImage>` persisted as an EF JSON-string column via **System.Text.Json** (`SpikerSoft.Data/Contexts/SpikerDbContext.cs:1059`). But `ProfileImage`'s storage-key properties are annotated with **Newtonsoft** `[JsonProperty]`: - `SpikerSoft.Data/Mongos/ProfileSettingsSubDocuments.cs:10` — `[JsonProperty("gridFsFileId")]` on `GridFsFileId` - `SpikerSoft.Data/Mongos/ProfileSettingsSubDocuments.cs:31` — `[JsonProperty("thumbnailGridFsId")]` on `ThumbnailGridFsId` System.Text.Json **ignores** `[JsonProperty]`. So these persist under their CLR names — PascalCase `"GridFsFileId"` / `"ThumbnailGridFsId"` — not the lowercase the attribute implies. The attribute is decorative and actively misleading. Unlike `ArtAsset.Artifacts`, this converter has **no** case-insensitive read option, so there is no safety net. ## Why it matters This is #852's exact failure mode, unexploded. It is dormant purely because nobody has renamed those CLR properties. The moment someone does — exactly the "harmless rename" #849 looked like — every profile image key deserializes to its default and avatars/photos break, silently, with every sibling field still binding correctly. `ProfileTravelSubDocuments.cs:38` (`[JsonProperty("gridFsId")]` on `GridFsId`) is in the same JSON-column situation. ## Note These are **genuinely still GridFS** — Profile/avatars have not migrated to MinIO — so this is *not* a rename-to-`objectKey` task. The fix is to make the persisted contract honest and enforced: - Replace the Newtonsoft `[JsonProperty]` attributes with `[JsonPropertyName]` matching the name **actually on disk today** (PascalCase), or migrate the documents and pin the new name — the former is far cheaper and non-breaking. - Consider a guard test in the spirit of `ArtStudioStorageKeyNamingTests` pinning the serialized names. - Audit the other 63 `HasConversion` JSON columns for the same Newtonsoft-attribute-on-an-STJ-column pattern. ## Related - #852 — the outage this pattern caused in Art Studio - #853 — the proper rename that removes the shim there
Author
Owner

Re-verified against origin/masternothing has changed. All three fix items outstanding. File last touched 2025-11-20.

The latent twin is still there verbatim:

  • ProfileSettingsSubDocuments.cs:10[JsonProperty("gridFsFileId")]
  • ProfileSettingsSubDocuments.cs:31[JsonProperty("thumbnailGridFsId")]
  • ProfileTravelSubDocuments.cs:38 — likewise

All Newtonsoft attributes on a type persisted through System.Text.Json, which is exactly the #852 mechanism: the attribute is inert, so the property serializes under its CLR name and any rename silently orphans the stored data.

Remaining, with the greps that show absence:

  1. No [JsonPropertyName] replacementgit grep -n "JsonPropertyName" origin/master -- '*ProfileSettingsSubDocuments.cs' '*ProfileTravelSubDocuments.cs' exits 1 with no output.
  2. No guard test pinning serialized namesgit grep -n "gridFsFileId" origin/master returns exactly one hit: the attribute itself. Nothing asserts the wire name.
  3. No safety net on the conversionUserProfile.Images still passes (JsonSerializerOptions?)null in both directions (SpikerDbContext.cs:1108-1112), so it has the same case-sensitive, silently-empty failure mode #852 had.

Also still outstanding: the ticket's broader ask to audit the other 63 HasConversion JSON columns for the same pattern. That audit has not been done, and it's the part that would tell us whether #852/#854 are two instances or the visible tip of a class.

Re-verified against `origin/master` — **nothing has changed. All three fix items outstanding.** File last touched 2025-11-20. The latent twin is still there verbatim: - `ProfileSettingsSubDocuments.cs:10` — `[JsonProperty("gridFsFileId")]` - `ProfileSettingsSubDocuments.cs:31` — `[JsonProperty("thumbnailGridFsId")]` - `ProfileTravelSubDocuments.cs:38` — likewise All Newtonsoft attributes on a type persisted through System.Text.Json, which is exactly the #852 mechanism: the attribute is inert, so the property serializes under its CLR name and any rename silently orphans the stored data. Remaining, with the greps that show absence: 1. **No `[JsonPropertyName]` replacement** — `git grep -n "JsonPropertyName" origin/master -- '*ProfileSettingsSubDocuments.cs' '*ProfileTravelSubDocuments.cs'` exits 1 with no output. 2. **No guard test pinning serialized names** — `git grep -n "gridFsFileId" origin/master` returns exactly one hit: the attribute itself. Nothing asserts the wire name. 3. **No safety net on the conversion** — `UserProfile.Images` still passes `(JsonSerializerOptions?)null` in both directions (`SpikerDbContext.cs:1108-1112`), so it has the same case-sensitive, silently-empty failure mode #852 had. Also still outstanding: the ticket's broader ask to audit the other **63** `HasConversion` JSON columns for the same pattern. That audit has not been done, and it's the part that would tell us whether #852/#854 are two instances or the visible tip of a class.
Sign in to join this conversation.