Skip to content

Fix double-encoding of non-string JSON values in nested blocks and add SingleBlock mapper#955

Merged
KevinJump merged 2 commits into
KevinJump:v17/mainfrom
koty10:fix/single-block-value-corruption
May 13, 2026
Merged

Fix double-encoding of non-string JSON values in nested blocks and add SingleBlock mapper#955
KevinJump merged 2 commits into
KevinJump:v17/mainfrom
koty10:fix/single-block-value-corruption

Conversation

@koty10
Copy link
Copy Markdown
Contributor

@koty10 koty10 commented May 13, 2026

Fix: Double-encoding of non-string JSON values inside block properties during import + missing SingleBlock mapper

Problem

When importing content that contains nested block structures (e.g. a BlockGrid with an area containing items that have SingleBlock properties with DropDown.Flexible fields), non-string JSON values get corrupted during import.

For example, a dropdown value ["Orange"] (a JSON array) is correctly stored in the serialized XML, but after import it becomes the JSON string "[\"Orange\"]" (double-encoded). This causes JsonReaderException errors when rendering the page and shows invalid values in the backoffice.

Root cause

SyncBlockMapperBase<T>.GetImportProperty() converts each block property value to a string via GetStringValue(), sends it through the mapper collection, and assigns the result back to BlockPropertyValue.Value. For editors without a dedicated mapper (e.g. Umbraco.DropDown.Flexible), the raw string is returned and assigned back. When the parent block is re-serialized via SerializeJsonString(), values that were originally JSON arrays/objects/numbers (but are now C# strings) get double-encoded.

The export side (GetExportProperty) already handles this correctly by converting the result back to a JsonNode:

return result.ConvertToJsonNode()?.ExpandAllJsonInToken() ?? result;

But the import side was missing this conversion.

Fix

  1. SyncBlockMapperBase.GetImportProperty() — When the original BlockPropertyValue.Value was a non-string JSON type (JsonElement with ValueKind of Array, Object, Number, etc.), the string result is now converted back to a JsonNode before being assigned back. This preserves the correct JSON type and prevents double-encoding. String-type values are left unchanged to avoid altering their type.

  2. New SingleBlockMapper — Added a mapper for Umbraco.SingleBlock (analogous to existing BlockListMapper and BlockGridMapper). Without this, properties inside SingleBlock editors were never recursively processed during import — UDIs in MultiUrlPickers, media references, etc. inside SingleBlock were not mapped, and the entire SingleBlock value was subject to the double-encoding issue.

Affected scenarios

Any property editor without a dedicated ISyncMapper that stores non-string JSON values (arrays, objects, numbers) inside any block type (BlockGrid, BlockList, SingleBlock). The most visible case is Umbraco.DropDown.Flexible which stores values as ["SelectedValue"].

How to reproduce

  1. Create a page with a BlockGrid containing blocks that have a SingleBlock field
  2. Inside the SingleBlock, add a block with a DropDown.Flexible property, select a value (e.g. "Orange")
  3. Export via uSync
  4. On a different instance (clean DB), import via uSync
  5. The dropdown value is corrupted — page rendering fails with JsonReaderException

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR fixes a double-encoding issue where non-string JSON values (e.g. ["Orange"] from Umbraco.DropDown.Flexible) inside nested block properties were being re-serialized as JSON strings on import, corrupting the value. It also introduces a missing SingleBlockMapper so that properties inside Umbraco.SingleBlock editors are recursively processed in the same way as BlockList and BlockGrid.

Changes:

  • In SyncBlockMapperBase.GetImportProperty, when the original value was a non-string JSON type, convert the mapped string result back to a JsonNode to preserve the original JSON shape.
  • Add a IsNonStringJsonValue helper that detects JsonElement (any ValueKind except String/Undefined) as well as JsonArray/JsonObject.
  • Add a new SingleBlockMapper registered via ISyncMapper discovery, mirroring BlockListMapper/BlockGridMapper but typed as SyncBlockMapperBase<SingleBlockValue>.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated no comments.

File Description
uSync.Core/Mapping/SyncBlockMapperBase.cs Adds JSON-type preservation on the import path to avoid double-encoding non-string values.
uSync.Core/Mapping/Mappers/SingleBlockMapper.cs New mapper enabling recursive import/export processing of Umbraco.SingleBlock properties.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

@KevinJump
Copy link
Copy Markdown
Owner

Hi Thanks for this, done some tests and it looks good 👍

  • i've just moved the static IsNonStringJsonValue into out JsonExtensions class, in case we want to use it somewhere else.

@KevinJump KevinJump merged commit 0ba2e0c into KevinJump:v17/main May 13, 2026
3 checks passed
@koty10 koty10 deleted the fix/single-block-value-corruption branch May 13, 2026 13:23
@KevinJump
Copy link
Copy Markdown
Owner

In release v17.3.2 - https://www.nuget.org/packages/uSync/17.3.2

alexsee pushed a commit to alexsee/umbraco-container that referenced this pull request May 21, 2026
Updated [uSync](https://github.com/KevinJump/uSync) from 17.3.0 to
17.3.2.

<details>
<summary>Release notes</summary>

_Sourced from [uSync's
releases](https://github.com/KevinJump/uSync/releases)._

## 17.3.2

This is a minor patch release of uSync for Umbraco v17, it contains
updates to how some values are encoded inside blocks and proper support
for SingleBlock elements.

## What's Changed
* Fix double-encoding of non-string JSON values in nested blocks and add
SingleBlock mapper by @​koty10 in
KevinJump/uSync#955

## New Contributors
* @​koty10 made their first contribution in
KevinJump/uSync#955

**Full Changelog**:
KevinJump/uSync@v17.3.1...v17.3.2

## 17.3.1

This is a Patch release for uSync for Umbraco v17.1. it contains fixes
and updates for known issues.:

the principle issue is uSync being to clever and trying to update the
EditorUIAlias based on best guess when in some situations (#​949) it
should just leave it alone

## What's Changed
* 👍 remove unused parameter on download, make it require admin at all
times (not just tree access) by @​KevinJump in
KevinJump/uSync#948
* 👍 update build script so we genete uSync.Extend now too. by
@​KevinJump in KevinJump/uSync#951
* 🐛 Fixes: #​949 - Property Editor UI alias values get overwritten on
import by @​KevinJump in KevinJump/uSync#950

**Full Changelog**:
KevinJump/uSync@v17.3.0...v17.3.1

### Installing
```
dotnet add package uSync --version 17.3.1
```

### Updating
> [NOTE!] with the new centralized package management feature being
using on v17.3+ umbraco projects you need to up date package versions
using dotnet package update, dotnet add package won't do it.

```
dotnet package update uSync --version 17.3.1
```

Commits viewable in [compare
view](KevinJump/uSync@v17.3.0...v17.3.2).
</details>

Updated [uSync.Complete](https://jumoo.co.uk/uSync/complete) from 17.3.2
to 17.3.6.

Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot show <dependency name> ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)


</details>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
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.

3 participants