Skip to content

Report 31 -- DropDown.Flexible Migration Analysis (DEFINITIVE)

Date: 2026-02-28 Priority: P0 -- Critical (crashes on dropdown reads) Week Plan Task: M1 (Marco) Status: ROOT CAUSE CONFIRMED from Umbraco v17 source code


Problem Statement

The migration tool produces dropdown values that crash Umbraco v17's FlexibleDropdownPropertyValueConverter. The root cause is a serialization mismatch: the migration tool embeds a native JSON array inside block JSON, but Umbraco v17 expects a JSON string (double-encoded array) at that position.


CORRECTION: V8 Source Format

Previous report was WRONG. V8 does NOT store dropdowns as plain strings.

V8 stores dropdown values as JSON arrays in umbracoPropertyData.varcharValue:

varcharValue = ["teal-blue-horizontal"]    -- single-select
varcharValue = ["value1","value2"]          -- multi-select

This was confirmed by direct SQL query against the v8 database (progresscu.ie). The Umbraco.DropDown.Flexible property editor in v8 serializes selected values as a JSON array string, regardless of whether multiple is true or false.


V17 Expected Format (from Umbraco Source)

Umbraco v17's FlexibleDropdownPropertyValueConverter (in Umbraco.Cms.Core) expects:

For direct properties (in umbracoPropertyData.textValue):

A JSON string of an array:

textValue = '["Left"]'           -- stored as string in the database column

When the property converter reads this, it calls JsonSerializer.Deserialize<string[]>(inter.ToString()) which correctly parses the JSON string into a string[].

For properties inside BlockGrid/BlockList JSON:

A JSON string value (double-encoded) inside the block JSON object:

{
  "contentData": [{
    "values": [{
      "alias": "imageLocation",
      "value": "[\"Left\"]"        <-- STRING value containing JSON array
    }]
  }]
}

The value field must be a string that happens to contain a JSON array. When Umbraco's block deserializer reads this, it treats it as an inter value and passes it to the property value converter, which then deserializes the string into string[].


The Bug: Native JSON Array vs JSON String

What the migration tool produces (WRONG):

GridValueTypeConverter.ConvertDropdownValue() at line ~658 returns a JArray object:

var normalizedValues = new JArray();
foreach (var val in extractedValues) {
    normalizedValues.Add(normalized);
}
return normalizedValues;  // Returns JArray OBJECT

When this JArray is embedded into the block JSON by GridContentMigrationService, it serializes as a native JSON array:

{
  "contentData": [{
    "values": [{
      "alias": "imageLocation",
      "value": ["Left"]            <-- NATIVE array, NOT a string
    }]
  }]
}

What Umbraco v17 expects (CORRECT):

{
  "contentData": [{
    "values": [{
      "alias": "imageLocation",
      "value": "[\"Left\"]"        <-- STRING containing JSON array
    }]
  }]
}

Why this crashes:

When FlexibleDropdownPropertyValueConverter receives the native array ["Left"], it tries to call .ToString() on it (which returns [\n "Left"\n] with formatting), then tries to JsonSerializer.Deserialize<string[]>() on that -- which may or may not work depending on the exact serialization path. In many code paths, the type mismatch causes a JsonException or returns unexpected values.

Double-encoding bug in database:

There is also evidence of a double-encoding bug where previously-migrated data has been re-processed, producing values like:

"[\"[\\\"purple-navy-horizontal\\\"]\"]"

This is a JSON array containing a string that itself contains a JSON array -- the result of the migration tool wrapping an already-wrapped value.


All Code Paths That Touch Dropdown Values

13 Methods Across 6 Classes

# Class Method What It Does Bug?
1 GridValueTypeConverter ConvertValue() Dispatcher -- routes to ConvertDropdownValue() for dropdown editors Entry point
2 GridValueTypeConverter ConvertDropdownValue() Parses v8 value, normalizes, returns JArray YES -- returns JArray instead of string
3 GridValueTypeConverter ExtractDropdownValues() Extracts values from various formats (string, array, object) OK
4 GridValueTypeConverter NormalizeDropdownValue() Trims, lowercases, removes brackets OK
5 DropDownConverter ConvertAsync() Registry converter -- unwraps single arrays to plain string Correct but overridden
6 DropDownConverter ParseDropdownArray() Parses JSON array from string OK
7 SqlMigrationService TransformDropdownPropertyValue() Direct property migration -- wraps string as ["value"] CORRECT (returns string)
8 SqlMigrationService TransformDataType() Routes to TransformDropdownPropertyValue() for dropdown data types OK
9 GridContentMigrationService MigrateGridContentAsync() Calls GridValueTypeConverter.ConvertValue() for each property Embeds returned value
10 GridContentMigrationService SetPropertyValue() Sets the value on the block JSON object Serializes JArray as native
11 GridConfigurationMigrationService CreateDropdownDataType() Creates the v17 dropdown data type config OK (config, not values)
12 BlockGridExtensions SafeDropdownValue() V17 view helper -- extracts string from JSON array Defense layer
13 CalculatorService GetDropdownValue() V17 calculator helper -- same as SafeDropdownValue Defense layer

The Fix

Root Cause Fix (Migration Tool)

File: GridValueTypeConverter.cs -- ConvertDropdownValue() (line ~658)

Change: Return a JSON string instead of a JArray object:

// BEFORE (buggy):
return normalizedValues;  // JArray object -- serializes as native array

// AFTER (fixed):
return normalizedValues.ToString(Formatting.None);  // String -- serializes as "["value"]"

This single-line change ensures the value is embedded as a string inside the block JSON, which is what Umbraco v17's property value converter expects.

Direct Properties Path (Already Correct)

File: SqlMigrationService.cs -- TransformDropdownPropertyValue() (line ~5812)

This path is already correct. It returns a (string result, ...) tuple, which gets written to textValue in the database. The value ["Left"] stored as a database string is exactly what v17 expects for direct properties.

V17 View Defense (Keep)

File: BlockGridExtensions.cs -- SafeDropdownValue() (line ~261)

This helper should be kept as a defense layer for already-migrated data that contains the native array format. It handles both formats:

public static string SafeDropdownValue(this IPublishedElement content, string alias, string fallback = "")
{
    try
    {
        var raw = content.Value<string>(alias);
        if (string.IsNullOrWhiteSpace(raw)) return fallback;

        // Try parse as JSON array
        if (raw.TrimStart().StartsWith("["))
        {
            var arr = JArray.Parse(raw);
            return arr.Count > 0 ? arr[0]?.ToString() ?? fallback : fallback;
        }
        return raw;
    }
    catch
    {
        return fallback;
    }
}

All Affected Dropdown Properties

Property Alias Used In Description
gradient ctaCurves, ctaText, heroItem, heroElement, splitPictureSlider, testimonialCardItem, headerNew, headerNews, services_list_image Background gradient class
buttonStyle ctaCurves, ctaText, headerNew, splitPictureSlider, NewsGlobalCTA CSS button class
imageLocation cardItem, cardGridControls, heroItem, heroElement, testimonialCardItem, _CardText, _CardItemContent Image position
flip flipCardControls, faqControls, faqElement Flip card animation
display announcementControls, loanboxescompletegrideditor, footerListControl Layout display mode
layout spotlightGridControls, cardGridControls Layout variant
layoutStyle spotlightControls Layout style
height standardPageWow, standardPageWithImageText, googleMap, largegooglemap, largeleafletmap Component height
buttonClass buttonWidgetControl, accordionButtonElement, accordionButtonMain, mainLoanBoxWidget, iconListOptContactInfoControl Button CSS class
barChartType barChart, barChartItem Chart type
cols flipCardControls Column count
iconPosition announcementItemScrollControls, announcementControls Icon placement
zoomLevel googleMap, largeleafletmap, blocklist/googleMap Map zoom
class ctaText, parallaxImageItem, faqItems, gridslider, headerNew CSS class
defaultLoanCalcFrequency calculatorControls Calculator frequency
alignTitle careerWidgetControl Title alignment
listStyle footerLinks List styling
dropDownListFirstOption releaseNotesWidget Dropdown label

Protection Status

Protected (26 locations) -- using SafeDropdownValue() or try/catch

File Property Method
announcementItemScrollControls.cshtml:20 display SafeDropdownValue()
calculatorControls.cshtml:35 defaultLoanCalcFrequency SafeDropdownValue()
careerWidgetControl.cshtml:28 alignTitle SafeDropdownValue()
ctaText.cshtml:21,23 buttonStyle, gradient SafeDropdownValue()
footerListControl.cshtml:13 display SafeDropdownValue()
splitPictureSlider.cshtml:31,32 gradient, buttonStyle SafeDropdownValue()
spotlightGridControls.cshtml:17 layout SafeDropdownValue()
heroItem.cshtml:24,26 imageLocation, gradient SafeDropdownValue()
heroElement.cshtml:23,25 imageLocation, gradient SafeDropdownValue()
spotlightControls.cshtml:19 layoutStyle SafeDropdownValue()
barChart.cshtml:20 barChartType try/catch
faqControls.cshtml:27 flip try/catch
faqElement.cshtml:15 flip try/catch
cardItem.cshtml:28 imageLocation try/catch
_CardText.cshtml:15 imageLocation try/catch
_CardItemContent.cshtml:15 imageLocation try/catch
testimonialCardItem.cshtml:25,33 imageLocation, gradient try/catch
buttonWidgetControl.cshtml:15 class try/catch
accordionButtonElement.cshtml:31,68 buttonClass try/catch
accordionButtonMain.cshtml:35,78 buttonClass try/catch
mainLoanBoxWidget.cshtml:159 buttonClass try/catch
standardPageWow.cshtml:31 height try/catch
blocklist/headerNew.cshtml:45,50 gradient, buttonStyle try/catch
blocklist/headerNews.cshtml:35 gradient try/catch
blocklist/googleMap.cshtml:27,30 zoomLevel, height try/catch
NewsGlobalCTA.cshtml:53,64 buttonStyle, gradient try/catch
grid_element_largeleafletmap.cshtml:30,33 zoomLevel, height try/catch

UNPROTECTED (18+ locations) -- WILL CRASH until migration tool fix is applied

File Property Risk
announcementControls.cshtml:13 display CRASH
announcementControls.cshtml:64 iconPosition CRASH
ctaCurves.cshtml:33,35 gradient CRASH
ctaCurves.cshtml:68,70 buttonStyle CRASH
flipCardControls.cshtml:18,20 flip CRASH
flipCardControls.cshtml:23,25 cols CRASH
googleMap.cshtml:23 zoomLevel CRASH
googleMap.cshtml:24 height CRASH
grid_element_loanboxescompletegrideditor.cshtml:17 display CRASH
grid_element_largegooglemap.cshtml:77 height CRASH
grid_element_services_list_image.cshtml:69 gradient CRASH
barChartItem.cshtml:23 barChartType CRASH
cardGridControls.cshtml:37,41,45 imageLocation, styling, contentAlignment CRASH
iconListOptContactInfoControl.cshtml:92,197 buttonClass CRASH
parallaxImageItem.cshtml:74,76 class CRASH
standardPageWithImageText.cshtml:26,27 height, buttonClass CRASH
releaseNotesWidget.cshtml:8,20 dropDownListFirstOption CRASH
footerLinks.cshtml:14 listStyle CRASH
faqItems.cshtml:18 class CRASH

Key Files

Migration Tool (Root Cause)

File Lines Role Fix Needed?
GridValueTypeConverter.cs 530-658 ConvertDropdownValue() returns JArray -- should return string YES -- line ~658
SqlMigrationService.cs 5812-5852 TransformDropdownPropertyValue() returns string Already correct
DropDownConverter.cs 36-127 Registry converter -- correct but overridden by GridValueTypeConverter No change needed
GridContentMigrationService.cs 489-494 Calls GridValueTypeConverter and embeds result in JSON No change needed

V17 Defense Layer (Keep)

File Lines Role
BlockGridExtensions.cs 261-277 SafeDropdownValue() helper -- defense for already-migrated data
CalculatorService.cs 220-243 GetDropdownValue() helper for calculators

Fix Plan

Step 1: Migration Tool Fix (1 line)

File: GridValueTypeConverter.cs -- ConvertDropdownValue() (~line 658)

// Change:
return normalizedValues;
// To:
return normalizedValues.ToString(Formatting.None);

Step 2: Re-run Migration

After the fix, re-run the full migration to regenerate all block content with correctly-encoded dropdown values.

Step 3: Verify

Query the v17 database to confirm dropdown values inside block JSON are stored as strings:

SELECT TOP 10 textValue
FROM umbracoPropertyData
WHERE textValue LIKE '%imageLocation%'

Expected: "value": "[\"Left\"]" (string), NOT "value": ["Left"] (native array).

Step 4: Keep SafeDropdownValue() as Defense

Even after the fix, keep SafeDropdownValue() in views as a defense layer. It handles both formats gracefully and protects against: - Already-migrated data that hasn't been re-migrated yet - Any edge cases in the double-encoding bug - Future-proofing against format changes


Week Plan Assignment

  • M1 (Marco): Apply the one-line fix to GridValueTypeConverter.ConvertDropdownValue() -- return .ToString(Formatting.None) instead of raw JArray
  • J3 (Joao): Add SafeDropdownValue() to all 18+ unprotected views (defense layer)
  • M7 (Marco): Re-run migration after fix and verify dropdown values are correctly double-encoded strings
Migration documentation by Double for Progress Credit Union