Skip to content

Use Jumoo.Json for the json helpers - #1014

Merged
KevinJump merged 1 commit into
v18/mainfrom
v18/jumoo-json
Jul 29, 2026
Merged

Use Jumoo.Json for the json helpers#1014
KevinJump merged 1 commit into
v18/mainfrom
v18/jumoo-json

Conversation

@KevinJump

Copy link
Copy Markdown
Owner

uSync.Core/Extensions/JsonTextExtensions.cs was a copy of the Jumoo.Json package and had drifted behind it. uSync's own code now calls Jumoo.Json 18.0.3 directly, so there is one implementation to maintain, and uSync inherits the fixes and allocation work done there.

Depends on Jumoo/Jumoo.Json#9 (merged, published as 18.0.3).

What uSync gains

TryConvertToJsonNode returned false for any string containing a quote, backslash or control character it built its fallback with JsonNode.Parse($"\"{value}\""), which is invalid json for those inputs
TrySerializeJsonNode built a fresh JsonSerializerOptions on every call options instances build their own metadata cache
TryExpandJsonNodeValue deep-cloned at every level of recursion 685µs/2.1MB → 162µs/399KB on a 203KB payload
IsJsonEqual / IsValidJsonString allocated on the large object heap ~196 and ~92 Gen2 collections per 1000 ops → zero
property accessors called ToString() on whole subtrees before parsing GetPropertyValueOrDefault on a type mismatch: 1,509ns → 10ns

Numbers are from Jumoo.Json.Benchmarks/README.md.

Nothing downstream needs to change

uSync.Core.Extensions.JsonTextExtensions keeps its namespace, class name, signatures and behaviour — including the places where Jumoo.Json returns null and it returned string.Empty. Every method is now [Obsolete] and forwards, and they will be removed in v20. JsonSerializerOptionsTests covers those contracts, since that is what stands between uSync.Complete and a broken build.

To move a file over, replace using uSync.Core.Extensions; with using Jumoo.Json;. A file can't have both — they declare the same extension method signatures, so a call matching both is ambiguous (CS0121). Names that differ: TryGetPropertyAsObjectTryGetPropertyAsJsonObject, GetPropertyAsObjectGetPropertyAsJsonObject.

How the migration handles that ambiguity

uSync.Core.Extensions holds only four classes; everything else lives in namespace uSync.Core. So of the 42 migrated files:

  • 35 simply swap the using.
  • 4 take a using static for the one other class they needed (DictionaryExtensions / ObjectPropertyExtensions / ConversionExtensions).
  • 2 are themselves inside namespace uSync.Core.Extensions, where the shim wins on namespace proximity no matter what the usings say, so they call Jumoo.Json.JsonSerialization.TryGetValueAs fully qualified.
  • GlobalUsings.cs aliases the public uSyncTaskHelper once, rather than repeating an alias in fourteen files.

Zero CS0618/CS0619 warnings solution-wide, so nothing in uSync still routes through the shim.

Serialized output

This is the risk that matters — uSync detects changes by comparing serialized json, so a formatting difference makes every item report as changed.

Jumoo.Json lists the same converters in a different order, and STJ resolves first-match-wins, so I checked rather than assumed: every type resolves to the same converter as before (notably JsonObjectConverter coming before JsonUdiConverter does not steal Udi). JsonXElementConverter is the same implementation under a new namespace. That check is now JsonSerializerOptionsTests, which doubles as a guard against another Jumoo package calling JsonTextOptions.AddConverter and quietly reformatting everyone's .config files — those options are a process-wide static shared across the Jumoo packages.

Behaviour changes I had to make by hand

  • RichTextEditorMigratingSerializer.TopLevelEditorSerializeJsonString returning null instead of string.Empty turned a continue into a dropped config key. Rewritten to fall through to the else branch as before.
  • uSyncHistoryNotificationHandler — now logs and skips instead of writing an empty history file if serialization fails.
  • ?? string.Empty where a caller needs non-null: ColourPickerMigratingConfigSerializer, RTEBlockDataContentMigrator, ContentSerializerBase, DataTypeTracker.
  • The config mergers already null-guarded DeserializeJson, which now returns default rather than throwing — the guards were already the intended path.

Also

  • Deletes uSync's copy of OrderedPropertiesJsonResolver (internal, byte identical upstream).
  • Obsoletes uSync.Core.Json.JsonXElementConverter.
  • SyncConfigMergerBase.cs:113's ToJsonArray() turned out to be Umbraco's own JsonNodeExtensions.ToJsonArray(IEnumerable<JsonNode?>), not uSync's string overload — untouched.

Testing

  • dotnet build uSync.slnx -c Release — clean, only the pre-existing CS8632 in TryGetValueAsTests.
  • dotnet test uSync.Tests — 177 passing (was 148).
  • dotnet pack uSync.Core — emits a single clean Jumoo.Json 18.0.3 dependency.

Still outstanding: the export diff. Run a full export from v18/main and from this branch against the same database and confirm git diff --no-index is empty, plus a report against a pre-change export showing zero changed items. The converter check above is strong evidence but it is not the same as a real export. Worth exercising block list/grid, an RTE with blocks, media picker 3 with crops, and a root data type config merge specifically.

🤖 Generated with Claude Code

uSync.Core/Extensions/JsonTextExtensions.cs was a copy of the Jumoo.Json
package, and had drifted behind it. uSync's own code now calls Jumoo.Json
(18.0.3) directly, so there is one implementation to maintain and uSync picks
up the fixes and allocation work done there:

- TryConvertToJsonNode returned false for any string containing a quote,
  backslash or control character - it built its fallback with
  JsonNode.Parse($"\"{value}\""), which is invalid json for those inputs.
- TrySerializeJsonNode built a fresh JsonSerializerOptions on every call.
- TryExpandJsonNodeValue deep-cloned at every level of recursion.
- IsJsonEqual and IsValidJsonString allocated on the large object heap for
  large values.

Nothing downstream needs to change. JsonTextExtensions keeps its namespace,
class name, signatures and behaviour - including where Jumoo.Json returns null
and it returned string.Empty - but every method is now [Obsolete] and forwards,
and they will be removed in v20. A file can't import both uSync.Core.Extensions
and Jumoo.Json, because they declare the same extension method signatures, so
the 42 migrated files import Jumoo.Json instead. Four take a using static for
the one other class they needed, two inside uSync.Core.Extensions call
Jumoo.Json fully qualified (the shim wins on namespace proximity there), and
GlobalUsings.cs aliases the public uSyncTaskHelper rather than repeating that
in fourteen files.

Also deletes uSync's copy of OrderedPropertiesJsonResolver (internal, byte
identical upstream) and obsoletes JsonXElementConverter.

Serialized output is unchanged. Jumoo.Json lists the converters in a different
order and order decides which one wins, so JsonSerializerOptionsTests pins the
converter uSync's serialization resolves for each type - that also guards
against another Jumoo package calling JsonTextOptions.AddConverter and quietly
reformatting everyone's .config files.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@KevinJump
KevinJump merged commit 22d3e36 into v18/main Jul 29, 2026
5 checks passed
@KevinJump
KevinJump deleted the v18/jumoo-json branch July 29, 2026 09:05
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant