@@ -679,12 +679,19 @@ fn unwrap_legacy_table_payloads(mut value: serde_json::Value) -> serde_json::Val
679679 * payload = elements. into_iter ( ) . next ( ) . unwrap_or ( serde_json:: Value :: Null ) ;
680680 }
681681 }
682- "Gradient" | "GradientTable" | "GradientPositions" => {
683- if let Some ( mut elements) = legacy_elements ( payload)
684- && let Some ( first) = elements. drain ( .. ) . next ( )
682+ "Gradient" | "GradientTable" | "GradientPositions" | "GradientStops" => {
683+ if let Some ( elements) = legacy_elements ( payload)
684+ && let Some ( first) = elements. into_iter ( ) . next ( )
685685 {
686686 * payload = first;
687687 }
688+
689+ // Pre-midpoint documents stored stops as a `[(position, color), ...]` array; convert to the struct form.
690+ if let Some ( stops) = payload. as_array ( )
691+ && let Some ( converted) = tuple_stops_to_struct ( stops)
692+ {
693+ * payload = converted;
694+ }
688695 }
689696 "BrushStrokes" | "BrushStrokeTable" => {
690697 if let Some ( elements) = legacy_elements ( payload) {
@@ -696,6 +703,31 @@ fn unwrap_legacy_table_payloads(mut value: serde_json::Value) -> serde_json::Val
696703 value
697704}
698705
706+ /// Converts the pre-midpoint `[(position, color), ...]` gradient-stops encoding into the modern
707+ /// `{ position, midpoint, color }` struct form. Returns `None` if the array isn't in that tuple
708+ /// shape (so an already-migrated struct payload, which deserializes as an object, is left alone).
709+ /// Midpoints default to `0.5`, matching the old hand-written `GradientStops` deserializer.
710+ #[ cfg( feature = "loading" ) ]
711+ fn tuple_stops_to_struct ( stops : & [ serde_json:: Value ] ) -> Option < serde_json:: Value > {
712+ let mut positions = Vec :: with_capacity ( stops. len ( ) ) ;
713+ let mut colors = Vec :: with_capacity ( stops. len ( ) ) ;
714+
715+ for stop in stops {
716+ let pair = stop. as_array ( ) ?;
717+ let [ position, color] = pair. as_slice ( ) else { return None } ;
718+ positions. push ( position. clone ( ) ) ;
719+ colors. push ( color. clone ( ) ) ;
720+ }
721+
722+ let midpoints = vec ! [ serde_json:: Value :: from( 0.5 ) ; stops. len( ) ] ;
723+
724+ Some ( serde_json:: json!( {
725+ "position" : positions,
726+ "midpoint" : midpoints,
727+ "color" : colors,
728+ } ) )
729+ }
730+
699731impl Display for TaggedValue {
700732 fn fmt ( & self , f : & mut std:: fmt:: Formatter < ' _ > ) -> std:: fmt:: Result {
701733 match self {
@@ -835,3 +867,40 @@ mod typedefault_dispatch {
835867 for_each_type_default ! ( check) ;
836868 }
837869}
870+
871+ #[ cfg( all( test, feature = "loading" ) ) ]
872+ mod legacy_gradient_migration {
873+ use super :: unwrap_legacy_table_payloads;
874+ use serde_json:: json;
875+
876+ /// Pre-midpoint documents stored gradient stops as `[(position, color), ...]`. After dropping the
877+ /// hand-written `GradientStops` deserializer, the migration layer must convert that tuple form into
878+ /// the `{ position, midpoint, color }` struct shape so old documents still load.
879+ #[ test]
880+ fn bare_tuple_stops_become_struct ( ) {
881+ let value = json ! ( { "Gradient" : [ [ 0.0 , "red" ] , [ 1.0 , "blue" ] ] } ) ;
882+
883+ let migrated = unwrap_legacy_table_payloads ( value) ;
884+
885+ assert_eq ! ( migrated, json!( { "Gradient" : { "position" : [ 0.0 , 1.0 ] , "midpoint" : [ 0.5 , 0.5 ] , "color" : [ "red" , "blue" ] } } ) ) ;
886+ }
887+
888+ /// The `LegacyTable`-wrapped tuple form (a one-row table holding the tuple-stops value) must unwrap
889+ /// the table first, then migrate the inner tuple array.
890+ #[ test]
891+ fn table_wrapped_tuple_stops_become_struct ( ) {
892+ let value = json ! ( { "GradientTable" : { "element" : [ [ [ 0.0 , "red" ] , [ 1.0 , "blue" ] ] ] } } ) ;
893+
894+ let migrated = unwrap_legacy_table_payloads ( value) ;
895+
896+ assert_eq ! ( migrated, json!( { "GradientTable" : { "position" : [ 0.0 , 1.0 ] , "midpoint" : [ 0.5 , 0.5 ] , "color" : [ "red" , "blue" ] } } ) ) ;
897+ }
898+
899+ /// An already-migrated struct payload must pass through untouched.
900+ #[ test]
901+ fn struct_stops_pass_through ( ) {
902+ let value = json ! ( { "Gradient" : { "position" : [ 0.0 ] , "midpoint" : [ 0.5 ] , "color" : [ "red" ] } } ) ;
903+
904+ assert_eq ! ( unwrap_legacy_table_payloads( value. clone( ) ) , value) ;
905+ }
906+ }
0 commit comments