From e0f3582199cc6ded22e0f4b1a4203ac6ff08b6fb Mon Sep 17 00:00:00 2001 From: koty10 Date: Wed, 13 May 2026 13:14:06 +0200 Subject: [PATCH 1/2] Fix double-encoding of non-string JSON values in nested blocks and add SingleBlock mapper --- .../Mapping/Mappers/SingleBlockMapper.cs | 23 +++++++++++++++++++ uSync.Core/Mapping/SyncBlockMapperBase.cs | 20 +++++++++++++++- 2 files changed, 42 insertions(+), 1 deletion(-) create mode 100644 uSync.Core/Mapping/Mappers/SingleBlockMapper.cs diff --git a/uSync.Core/Mapping/Mappers/SingleBlockMapper.cs b/uSync.Core/Mapping/Mappers/SingleBlockMapper.cs new file mode 100644 index 00000000..97b0371b --- /dev/null +++ b/uSync.Core/Mapping/Mappers/SingleBlockMapper.cs @@ -0,0 +1,23 @@ +using Microsoft.Extensions.Logging; + +using Umbraco.Cms.Core; +using Umbraco.Cms.Core.Models.Blocks; +using Umbraco.Cms.Core.Services; + +namespace uSync.Core.Mapping.Mappers; + +internal class SingleBlockMapper : SyncBlockMapperBase, ISyncMapper +{ + public override string Name => "NuBlock Single Block mapper"; + + public override string[] Editors => [Constants.PropertyEditors.Aliases.SingleBlock]; + + public SingleBlockMapper( + IEntityService entityService, + IContentTypeService contentTypeService, + Lazy mapperCollection, + ILogger logger) + : base(entityService, contentTypeService, mapperCollection, logger) + { + } +} diff --git a/uSync.Core/Mapping/SyncBlockMapperBase.cs b/uSync.Core/Mapping/SyncBlockMapperBase.cs index 68c511a2..756db363 100644 --- a/uSync.Core/Mapping/SyncBlockMapperBase.cs +++ b/uSync.Core/Mapping/SyncBlockMapperBase.cs @@ -1,6 +1,7 @@ using Microsoft.Extensions.Logging; using System.Collections; +using System.Text.Json; using System.Text.Json.Nodes; using Umbraco.Cms.Core; @@ -61,9 +62,26 @@ public SyncBlockMapperBase( _logger.LogDebug("Importing block value for {PropertyEditorAlias} {valueType}", propertyType.PropertyEditorAlias, value?.GetType().Name ?? "blank"); var importString = SyncBlockMapperBase.GetStringValue(value) ?? string.Empty; - return await _mapperCollection.Value.GetImportValueAsync(importString, propertyType, options); + var result = await _mapperCollection.Value.GetImportValueAsync(importString, propertyType, options); + + // When the original value was a non-string JSON type (array, object, number, etc.), + // convert string results back to JsonNode to preserve the correct JSON type + // and prevent double-encoding when the block value is re-serialized. + if (result is string stringResult && IsNonStringJsonValue(value)) + { + return stringResult.ConvertToJsonNode() ?? result; + } + + return result; } + /// + /// checks if the value is a non-string JSON value (array, object, number, boolean). + /// + private static bool IsNonStringJsonValue(object? value) + => value is JsonElement { ValueKind: not JsonValueKind.String and not JsonValueKind.Undefined } + || value is JsonArray or JsonObject; + private async Task GetExportProperty(object? value, IPropertyType? propertyType, SyncSerializerOptions options) { if (_mapperCollection.Value is null || propertyType is null) From aed7a32d849e650945d359b2643deacd93f30e76 Mon Sep 17 00:00:00 2001 From: Kevin Jump Date: Wed, 13 May 2026 14:14:06 +0100 Subject: [PATCH 2/2] move the IsNonStringJsonValue method into the Json Extensions class --- uSync.Core/Extensions/JsonTextExtensions.cs | 11 +++++++++++ uSync.Core/Mapping/SyncBlockMapperBase.cs | 9 +-------- 2 files changed, 12 insertions(+), 8 deletions(-) diff --git a/uSync.Core/Extensions/JsonTextExtensions.cs b/uSync.Core/Extensions/JsonTextExtensions.cs index 6e012b53..cd6029aa 100644 --- a/uSync.Core/Extensions/JsonTextExtensions.cs +++ b/uSync.Core/Extensions/JsonTextExtensions.cs @@ -520,4 +520,15 @@ public static bool IsJsonEqual(this object currentObject, object newObject) #endregion + #region Type Checks + + /// + /// checks if the value is a non-string JSON value (array, object, number, boolean). + /// + public static bool IsNonStringJsonValue(this object? value) + => value is JsonElement { ValueKind: not JsonValueKind.String and not JsonValueKind.Undefined } + || value is JsonArray or JsonObject; + + #endregion + } diff --git a/uSync.Core/Mapping/SyncBlockMapperBase.cs b/uSync.Core/Mapping/SyncBlockMapperBase.cs index 756db363..b9d24030 100644 --- a/uSync.Core/Mapping/SyncBlockMapperBase.cs +++ b/uSync.Core/Mapping/SyncBlockMapperBase.cs @@ -67,7 +67,7 @@ public SyncBlockMapperBase( // When the original value was a non-string JSON type (array, object, number, etc.), // convert string results back to JsonNode to preserve the correct JSON type // and prevent double-encoding when the block value is re-serialized. - if (result is string stringResult && IsNonStringJsonValue(value)) + if (result is string stringResult && value.IsNonStringJsonValue()) { return stringResult.ConvertToJsonNode() ?? result; } @@ -75,13 +75,6 @@ public SyncBlockMapperBase( return result; } - /// - /// checks if the value is a non-string JSON value (array, object, number, boolean). - /// - private static bool IsNonStringJsonValue(object? value) - => value is JsonElement { ValueKind: not JsonValueKind.String and not JsonValueKind.Undefined } - || value is JsonArray or JsonObject; - private async Task GetExportProperty(object? value, IPropertyType? propertyType, SyncSerializerOptions options) { if (_mapperCollection.Value is null || propertyType is null)