@@ -6,6 +6,7 @@ package morph
66import (
77 "fmt"
88 "math/big"
9+ "sort"
910 "strconv"
1011 "strings"
1112
@@ -569,6 +570,18 @@ func morphMapToType(v tftypes.Value, t tftypes.Type, p *tftypes.AttributePath) (
569570 }
570571 nmvals [k ] = nv
571572 }
573+ if t .(tftypes.Map ).ElementType .Is (tftypes .DynamicPseudoType ) {
574+ nmvals , err = NormalizeDynamicMapElements (nmvals , p )
575+ if err != nil {
576+ diags = append (diags , & tfprotov5.Diagnostic {
577+ Attribute : p ,
578+ Severity : tfprotov5 .DiagnosticSeverityError ,
579+ Summary : "Failed to normalize map(dynamic) element types" ,
580+ Detail : err .Error (),
581+ })
582+ return tftypes.Value {}, diags
583+ }
584+ }
572585 return newValue (t , nmvals , p )
573586 case t .Is (tftypes .DynamicPseudoType ):
574587 return v , diags
@@ -674,6 +687,18 @@ func morphObjectToType(v tftypes.Value, t tftypes.Type, p *tftypes.AttributePath
674687 }
675688 mvals [k ] = nv
676689 }
690+ if t .(tftypes.Map ).ElementType .Is (tftypes .DynamicPseudoType ) {
691+ mvals , err = NormalizeDynamicMapElements (mvals , p )
692+ if err != nil {
693+ diags = append (diags , & tfprotov5.Diagnostic {
694+ Attribute : p ,
695+ Severity : tfprotov5 .DiagnosticSeverityError ,
696+ Summary : "Failed to normalize map(dynamic) element types" ,
697+ Detail : err .Error (),
698+ })
699+ return tftypes.Value {}, diags
700+ }
701+ }
677702 return newValue (t , mvals , p )
678703 case t .Is (tftypes .DynamicPseudoType ):
679704 return v , diags
@@ -754,6 +779,217 @@ func newValue(t tftypes.Type, val interface{}, p *tftypes.AttributePath) (tftype
754779 return tftypes .NewValue (t , val ), nil
755780}
756781
782+ // NormalizeDynamicMapElements normalizes the value set for a map(dynamic) so all
783+ // elements have a coherent type shape before serialization.
784+ //
785+ // If all element types already match, values are returned unchanged.
786+ // If elements are all objects with differing attribute sets/types, element
787+ // object types are merged by attribute name and missing/incompatible attributes
788+ // are represented as DynamicPseudoType.
789+ // If elements are not all objects and have differing types, an error is
790+ // returned rather than coercing values.
791+ func NormalizeDynamicMapElements (vals map [string ]tftypes.Value , p * tftypes.AttributePath ) (map [string ]tftypes.Value , error ) {
792+ if p == nil {
793+ p = tftypes .NewAttributePath ()
794+ }
795+ if len (vals ) == 0 {
796+ return vals , nil
797+ }
798+
799+ var firstType tftypes.Type
800+ allSameType := true
801+ allObjectTypes := true
802+ for _ , v := range vals {
803+ if firstType == nil {
804+ firstType = v .Type ()
805+ } else if ! firstType .Equal (v .Type ()) {
806+ allSameType = false
807+ }
808+ if ! v .Type ().Is (tftypes.Object {}) {
809+ allObjectTypes = false
810+ }
811+ }
812+ if allSameType {
813+ return vals , nil
814+ }
815+ if ! allObjectTypes {
816+ return nil , p .NewErrorf (
817+ "[%s] cannot normalize map(dynamic) with incompatible element types: %s" ,
818+ attributePathSummary (p ),
819+ describeMapElementTypes (vals ),
820+ )
821+ }
822+
823+ mergedObjectType := mergeDynamicMapObjectType (vals )
824+ mergedAttrTypes := mergedObjectType .AttributeTypes
825+ normalized := make (map [string ]tftypes.Value , len (vals ))
826+
827+ for key , v := range vals {
828+ ep := p .WithElementKeyString (key )
829+ var objVals map [string ]tftypes.Value
830+ if err := v .As (& objVals ); err != nil {
831+ return nil , ep .NewError (err )
832+ }
833+
834+ outVals := make (map [string ]tftypes.Value , len (mergedAttrTypes ))
835+ for attrName , targetType := range mergedAttrTypes {
836+ ap := ep .WithAttributeName (attrName )
837+ if av , ok := objVals [attrName ]; ok {
838+ nv , err := normalizeMapObjectAttributeValue (av , targetType , ap )
839+ if err != nil {
840+ return nil , err
841+ }
842+ outVals [attrName ] = nv
843+ continue
844+ }
845+ outVals [attrName ] = tftypes .NewValue (targetType , nil )
846+ }
847+ normalized [key ] = tftypes .NewValue (mergedObjectType , outVals )
848+ }
849+
850+ return normalized , nil
851+ }
852+
853+ // NormalizeDynamicMapShapes recursively normalizes map(dynamic) values nested in
854+ // the supplied value to ensure serialization-safe element type structures.
855+ func NormalizeDynamicMapShapes (v tftypes.Value , p * tftypes.AttributePath ) (tftypes.Value , error ) {
856+ if p == nil {
857+ p = tftypes .NewAttributePath ()
858+ }
859+ if ! v .IsKnown () || v .IsNull () {
860+ return v , nil
861+ }
862+
863+ switch {
864+ case v .Type ().Is (tftypes.Object {}):
865+ var vals map [string ]tftypes.Value
866+ if err := v .As (& vals ); err != nil {
867+ return tftypes.Value {}, p .NewError (err )
868+ }
869+ atts := v .Type ().(tftypes.Object ).AttributeTypes
870+ ovals := make (map [string ]tftypes.Value , len (atts ))
871+ for name , attType := range atts {
872+ np := p .WithAttributeName (name )
873+ cv , ok := vals [name ]
874+ if ! ok {
875+ ovals [name ] = tftypes .NewValue (attType , nil )
876+ continue
877+ }
878+ nv , err := NormalizeDynamicMapShapes (cv , np )
879+ if err != nil {
880+ return tftypes.Value {}, err
881+ }
882+ ovals [name ] = nv
883+ }
884+ return tftypes .NewValue (v .Type (), ovals ), nil
885+
886+ case v .Type ().Is (tftypes.Map {}):
887+ var vals map [string ]tftypes.Value
888+ if err := v .As (& vals ); err != nil {
889+ return tftypes.Value {}, p .NewError (err )
890+ }
891+ mvals := make (map [string ]tftypes.Value , len (vals ))
892+ for key , el := range vals {
893+ np := p .WithElementKeyString (key )
894+ nv , err := NormalizeDynamicMapShapes (el , np )
895+ if err != nil {
896+ return tftypes.Value {}, err
897+ }
898+ mvals [key ] = nv
899+ }
900+ if v .Type ().(tftypes.Map ).ElementType .Is (tftypes .DynamicPseudoType ) {
901+ var err error
902+ mvals , err = NormalizeDynamicMapElements (mvals , p )
903+ if err != nil {
904+ return tftypes.Value {}, err
905+ }
906+ }
907+ return tftypes .NewValue (v .Type (), mvals ), nil
908+
909+ case v .Type ().Is (tftypes.List {}) || v .Type ().Is (tftypes.Set {}) || v .Type ().Is (tftypes.Tuple {}):
910+ var vals []tftypes.Value
911+ if err := v .As (& vals ); err != nil {
912+ return tftypes.Value {}, p .NewError (err )
913+ }
914+ out := make ([]tftypes.Value , len (vals ))
915+ for i , el := range vals {
916+ np := p .WithElementKeyInt (i )
917+ nv , err := NormalizeDynamicMapShapes (el , np )
918+ if err != nil {
919+ return tftypes.Value {}, err
920+ }
921+ out [i ] = nv
922+ }
923+ return tftypes .NewValue (v .Type (), out ), nil
924+ }
925+
926+ return v , nil
927+ }
928+
929+ func mergeDynamicMapObjectType (vals map [string ]tftypes.Value ) tftypes.Object {
930+ attrType := make (map [string ]tftypes.Type )
931+ attrPresentCount := make (map [string ]int )
932+ attrTypeConsistent := make (map [string ]bool )
933+ totalElements := len (vals )
934+
935+ for _ , v := range vals {
936+ objType := v .Type ().(tftypes.Object )
937+ for name , t := range objType .AttributeTypes {
938+ attrPresentCount [name ]++
939+ if priorType , ok := attrType [name ]; ! ok {
940+ attrType [name ] = t
941+ attrTypeConsistent [name ] = true
942+ } else if ! priorType .Equal (t ) {
943+ attrTypeConsistent [name ] = false
944+ }
945+ }
946+ }
947+
948+ merged := make (map [string ]tftypes.Type , len (attrType ))
949+ for name , t := range attrType {
950+ if attrPresentCount [name ] != totalElements || ! attrTypeConsistent [name ] {
951+ merged [name ] = tftypes .DynamicPseudoType
952+ continue
953+ }
954+ merged [name ] = t
955+ }
956+ return tftypes.Object {AttributeTypes : merged }
957+ }
958+
959+ func normalizeMapObjectAttributeValue (v tftypes.Value , targetType tftypes.Type , p * tftypes.AttributePath ) (tftypes.Value , error ) {
960+ if targetType .Is (tftypes .DynamicPseudoType ) {
961+ return v , nil
962+ }
963+ if v .Type ().Equal (targetType ) {
964+ return v , nil
965+ }
966+ if v .IsNull () {
967+ return tftypes .NewValue (targetType , nil ), nil
968+ }
969+ if ! v .IsKnown () {
970+ return tftypes .NewValue (targetType , tftypes .UnknownValue ), nil
971+ }
972+ return tftypes.Value {}, p .NewErrorf (
973+ "[%s] incompatible map(dynamic) attribute type: expected %s, got %s" ,
974+ attributePathSummary (p ),
975+ typeNameNoPrefix (targetType ),
976+ typeNameNoPrefix (v .Type ()),
977+ )
978+ }
979+
980+ func describeMapElementTypes (vals map [string ]tftypes.Value ) string {
981+ types := make (map [string ]struct {}, len (vals ))
982+ for _ , v := range vals {
983+ types [typeNameNoPrefix (v .Type ())] = struct {}{}
984+ }
985+ list := make ([]string , 0 , len (types ))
986+ for t := range types {
987+ list = append (list , t )
988+ }
989+ sort .Strings (list )
990+ return strings .Join (list , ", " )
991+ }
992+
757993// MorphTypeStructure converts a value to have a different type structure while preserving
758994// the underlying data. This is needed when plan and apply produce values with different
759995// type structures due to DynamicPseudoType being converted to concrete types during
0 commit comments