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/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..b9d24030 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,7 +62,17 @@ 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 && value.IsNonStringJsonValue()) + { + return stringResult.ConvertToJsonNode() ?? result; + } + + return result; } private async Task GetExportProperty(object? value, IPropertyType? propertyType, SyncSerializerOptions options)