Repository: goadesign/goa
Pull request #3080 (2022-06-13 20:21:52)

Properly handle complex union types
during marshaling/unmarshaling


Patch:
diff --git a/codegen/go_transform.go b/codegen/go_transform.go
index cc2a29cb..db1900fd 100644
--- a/codegen/go_transform.go
+++ b/codegen/go_transform.go
@@ -394,15 +394,15 @@ func transformUnionToObject(source, target *expr.AttributeExpr, sourceVar, targe
 	if (*obj)[0].Attribute.Type != expr.String {
 		return "", fmt.Errorf("union to object transform requires first field to be string")
 	}
-	if (*obj)[1].Attribute.Type != expr.Any {
-		return "", fmt.Errorf("union to object transform requires second field to be any")
+	if (*obj)[1].Attribute.Type != expr.String {
+		return "", fmt.Errorf("union to object transform requires second field to be string")
 	}
 	srcUnion := expr.AsUnion(source.Type)
 	sourceTypeRefs := make([]string, len(srcUnion.Values))
 	sourceTypeNames := make([]string, len(srcUnion.Values))
 	for i, st := range srcUnion.Values {
 		sourceTypeRefs[i] = ta.SourceCtx.Scope.Ref(st.Attribute, ta.SourceCtx.Pkg)
-		sourceTypeNames[i] = ta.SourceCtx.Scope.Name(st.Attribute, "", false, false)
+		sourceTypeNames[i] = st.Name
 	}
 	data := map[string]interface{}{
 		"NewVar":          newVar,
@@ -425,8 +425,8 @@ func transformObjectToUnion(source, target *expr.AttributeExpr, sourceVar, targe
 	if (*obj)[0].Attribute.Type != expr.String {
 		return "", fmt.Errorf("union to object transform requires first field to be string")
 	}
-	if (*obj)[1].Attribute.Type != expr.Any {
-		return "", fmt.Errorf("union to object transform requires second field to be any")
+	if (*obj)[1].Attribute.Type != expr.String {
+		return "", fmt.Errorf("union to object transform requires second field to be string")
 	}
 
 	sourceVarDeref := sourceVar
@@ -434,21 +434,22 @@ func transformObjectToUnion(source, target *expr.AttributeExpr, sourceVar, targe
 		sourceVarDeref = "*" + sourceVar
 	}
 	tgtUnion := expr.AsUnion(target.Type)
-	targetTypeNames := make([]string, len(tgtUnion.Values))
+	unionTypes := make([]string, len(tgtUnion.Values))
 	targetTypeRefs := make([]string, len(tgtUnion.Values))
 	for i, tt := range tgtUnion.Values {
-		targetTypeNames[i] = ta.TargetCtx.Scope.Name(tt.Attribute, ta.TargetCtx.Pkg, ta.TargetCtx.Pointer, ta.TargetCtx.Pointer)
+		unionTypes[i] = tt.Name
 		targetTypeRefs[i] = ta.TargetCtx.Scope.Ref(tt.Attribute, ta.TargetCtx.Pkg)
 	}
 	data := map[string]interface{}{
-		"NewVar":          newVar,
-		"TargetVar":       targetVar,
-		"TypeRef":         ta.TargetCtx.Scope.Ref(target, ta.TargetCtx.Pkg),
-		"SourceVar":       sourceVar,
-		"SourceVarDeref":  sourceVarDeref,
-		"TargetTypeNames": targetTypeNames,
-		"TargetTypeRefs":  targetTypeRefs,
-		"TargetTypeName":  ta.TargetCtx.Scope.Name(target, ta.TargetCtx.Pkg, ta.TargetCtx.Pointer, ta.TargetCtx.UseDefault),
+		"NewVar":         newVar,
+		"TargetVar":      targetVar,
+		"TypeRef":        ta.TargetCtx.Scope.Ref(target, ta.TargetCtx.Pkg),
+		"SourceVar":      sourceVar,
+		"SourceVarDeref": sourceVarDeref,
+		"UnionTypes":     unionTypes,
+		"TargetTypeRefs": targetTypeRefs,
+		"TargetTypeName": ta.TargetCtx.Scope.Name(target, ta.TargetCtx.Pkg, ta.TargetCtx.Pointer, ta.TargetCtx.UseDefault),
+		"Pointer":        ta.SourceCtx.Pointer,
 	}
 	var buf bytes.Buffer
 	if err := transformGoObjectToUnionT.Execute(&buf, data); err != nil {
@@ -667,7 +668,8 @@ for key, val := range {{ .SourceVar }} {
 `
 
 	transformGoUnionToObjectTmpl = `{{ if .NewVar }}var {{ .TargetVar }} {{ .TypeRef }}
-{{ end }}var name string
+{{ end }}js, _ := json.Marshal({{ .SourceVar }})
+var name string
 switch {{ .SourceVar }}.(type) {
 	{{- range $i, $ref := .SourceTypeRefs }}
 	case {{ $ref }}:
@@ -676,15 +678,17 @@ switch {{ .SourceVar }}.(type) {
 }
 {{ .TargetVar }} = &{{ .TargetTypeName }}{
 	Type: name,
-	Value: {{ .SourceVar }},
+	Value: string(js),
 }
 `
 
 	transformGoObjectToUnionTmpl = `{{ if .NewVar }}var {{ .TargetVar }} {{ .TypeRef }}
 {{ end }}switch {{ .SourceVarDeref }}.Type {
-	{{- range $i, $name := .TargetTypeNames }}
+	{{- range $i, $name := .UnionTypes }}
 	case {{ printf "%q" $name }}:
-		{{ $.TargetVar }} = {{ $.SourceVar }}.Value.({{ index $.TargetTypeRefs $i }})
+		var val {{ index $.TargetTypeRefs $i }}
+		json.Unmarshal([]byte({{ if $.Pointer }}*{{ end }}{{ $.SourceVar }}.Value), &val)
+		{{ $.TargetVar }} = val
 	{{- end }}
 }
 `
diff --git a/expr/http_body_types.go b/expr/http_body_types.go
index a981a458..e283f933 100644
--- a/expr/http_body_types.go
+++ b/expr/http_body_types.go
@@ -297,15 +297,16 @@ func unionToObject(att *AttributeExpr, name, suffix, svcName string) *AttributeE
 		names[i] = nat.Attribute.Type.Name()
 		vals[i] = fmt.Sprintf("- %q", nat.Attribute.Type.Name())
 	}
-	obj := Object([]*NamedAttributeExpr{
-		{"Type", &AttributeExpr{
+	obj := Object([]*NamedAttributeExpr{{
+		"Type", &AttributeExpr{
 			Type:        String,
 			Description: "Union type name, one of:\n" + strings.Join(vals, "\n"),
 			Validation:  &ValidationExpr{Values: names},
-		}},
-		{"Value", &AttributeExpr{
-			Type:        Any,
-			Description: "Union value, type must be one of service package types listed above",
+		}}, {
+		"Value", &AttributeExpr{
+			Type:         String,
+			Description:  "JSON formatted union value",
+			UserExamples: []*ExampleExpr{{Value: `"JSON"`}},
 		}},
 	})
 	uatt := &AttributeExpr{
diff --git a/go.sum b/go.sum
index aedd45c2..a274269a 100644
--- a/go.sum
+++ b/go.sum
@@ -108,13 +108,11 @@ github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5
 github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
 github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
 github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
-github.com/yuin/goldmark v1.4.1/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
 github.com/zach-klippenstein/goregen v0.0.0-20160303162051-795b5e3961ea h1:CyhwejzVGvZ3Q2PSbQ4NRRYn+ZWv5eS1vlaEusT+bAI=
 github.com/zach-klippenstein/goregen v0.0.0-20160303162051-795b5e3961ea/go.mod h1:eNr558nEUjP8acGw8FFjTeWvSgU1stO7FAO6eknhHe4=
 go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI=
 golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
 golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
-golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
 golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
 golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
 golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
@@ -127,12 +125,9 @@ golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73r
 golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
 golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
 golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
-golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
 golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
 golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
-golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
 golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
-golang.org/x/net v0.0.0-20211015210444-4f30a5c0130f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
 golang.org/x/net v0.0.0-20220531201128-c960675eff93 h1:MYimHLfoXEpOhqd/zgoA/uoXzHB86AEky4LAx5ij9xA=
 golang.org/x/net v0.0.0-20220531201128-c960675eff93/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
 golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
@@ -141,7 +136,6 @@ golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJ
 golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
-golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
 golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
 golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@@ -150,18 +144,13 @@ golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7w
 golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.0.0-20211019181941-9d821ace8654/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a h1:dGzPydgVsqGcTRVwiLJ1jVbufYwmzD3LfVPLKsKg+0k=
 golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
-golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
 golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
 golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
 golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
-golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
 golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk=
 golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
 golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
diff --git a/http/codegen/client.go b/http/codegen/client.go
index 424594bd..436e37ee 100644
--- a/http/codegen/client.go
+++ b/http/codegen/client.go
@@ -98,6 +98,7 @@ func clientEncodeDecodeFile(genpkg string, svc *expr.HTTPServiceExpr) *codegen.F
 	imports := []*codegen.ImportSpec{
 		{Path: "bytes"},
 		{Path: "context"},
+		{Path: "encoding/json"},
 		{Path: "fmt"},
 		{Path: "io"},
 		{Path: "io/ioutil"},
diff --git a/http/codegen/service_data.go b/http/codegen/service_data.go
index 326d4db1..e637c890 100644
--- a/http/codegen/service_data.go
+++ b/http/codegen/service_data.go
@@ -982,9 +982,10 @@ func makeHTTPType(att *expr.AttributeExpr, seen ...map[string]struct{}) {
 				},
 			}},
 			{Name: "Value", Attribute: &expr.AttributeExpr{
-				Type:        expr.Any,
-				Description: "Union value, type must be one of service package types listed above",
-				Bases:       bases, // For OpenAPI generation
+				Type:         expr.String,
+				Description:  "JSON formatted union value",
+				UserExamples: []*expr.ExampleExpr{{Value: `"JSON"`}},
+				Bases:        bases, // For OpenAPI generation
 				Meta: expr.MetaExpr{
 					"struct:tag:form": {"Value"},
 					"struct:tag:json": {"Value"},


Test patch:
diff --git a/codegen/go_transform_union_test.go b/codegen/go_transform_union_test.go
index 50a8507d..3a84ac74 100644
--- a/codegen/go_transform_union_test.go
+++ b/codegen/go_transform_union_test.go
@@ -97,36 +97,39 @@ const unionToUnionCode = `func transform() {
 
 const unionStringToUserTypeCode = `func transform() {
 	var target *UnionUserType
+	js, _ := json.Marshal(source)
 	var name string
 	switch source.(type) {
 	case UnionStringString:
-		name = "UnionStringString"
+		name = "String"
 	}
 	target = &UnionUserType{
 		Type:  name,
-		Value: source,
+		Value: string(js),
 	}
 }
 `
 
 const unionStringIntToUserTypeCode = `func transform() {
 	var target *UnionUserType
+	js, _ := json.Marshal(source)
 	var name string
 	switch source.(type) {
 	case UnionStringIntString:
-		name = "UnionStringIntString"
+		name = "String"
 	case UnionStringIntInt:
-		name = "UnionStringIntInt"
+		name = "Int"
 	}
 	target = &UnionUserType{
 		Type:  name,
-		Value: source,
+		Value: string(js),
 	}
 }
 `
 
 const unionSomeTypeToUserTypeCode = `func transform() {
 	var target *UnionUserType
+	js, _ := json.Marshal(source)
 	var name string
 	switch source.(type) {
 	case *SomeType:
@@ -134,7 +137,7 @@ const unionSomeTypeToUserTypeCode = `func transform() {
 	}
 	target = &UnionUserType{
 		Type:  name,
-		Value: source,
+		Value: string(js),
 	}
 }
 `
@@ -142,8 +145,10 @@ const unionSomeTypeToUserTypeCode = `func transform() {
 const userTypeToUnionStringCode = `func transform() {
 	var target *UnionString
 	switch source.Type {
-	case "UnionStringString":
-		target = source.Value.(UnionStringString)
+	case "String":
+		var val UnionStringString
+		json.Unmarshal([]byte(source.Value), &val)
+		target = val
 	}
 }
 `
@@ -151,10 +156,14 @@ const userTypeToUnionStringCode = `func transform() {
 const userTypeToUnionStringIntCode = `func transform() {
 	var target *UnionStringInt
 	switch source.Type {
-	case "UnionStringIntString":
-		target = source.Value.(UnionStringIntString)
-	case "UnionStringIntInt":
-		target = source.Value.(UnionStringIntInt)
+	case "String":
+		var val UnionStringIntString
+		json.Unmarshal([]byte(source.Value), &val)
+		target = val
+	case "Int":
+		var val UnionStringIntInt
+		json.Unmarshal([]byte(source.Value), &val)
+		target = val
 	}
 }
 `
@@ -163,7 +172,9 @@ const userTypeToUnionSomeTypeCode = `func transform() {
 	var target *UnionSomeType
 	switch source.Type {
 	case "SomeType":
-		target = source.Value.(*SomeType)
+		var val *SomeType
+		json.Unmarshal([]byte(source.Value), &val)
+		target = val
 	}
 }
 `
diff --git a/codegen/testdata/union_dsls.go b/codegen/testdata/union_dsls.go
index 5b99cf92..3d031012 100644
--- a/codegen/testdata/union_dsls.go
+++ b/codegen/testdata/union_dsls.go
@@ -41,7 +41,7 @@ var TestUnionDSL = func() {
 
 		_ = Type("UnionUserType", func() {
 			Attribute("Type", String)
-			Attribute("Value", Any)
+			Attribute("Value", String)
 			Required("Type", "Value")
 		})
 	)
diff --git a/http/codegen/testdata/payload_constructor_functions.go b/http/codegen/testdata/payload_constructor_functions.go
index a707bee3..84fba9ba 100644
--- a/http/codegen/testdata/payload_constructor_functions.go
+++ b/http/codegen/testdata/payload_constructor_functions.go
@@ -809,10 +809,14 @@ func NewMethodBodyUnionUnion(body *MethodBodyUnionRequestBody) *servicebodyunion
 	v := &servicebodyunion.Union{}
 	if body.Values != nil {
 		switch *body.Values.Type {
-		case "servicebodyunion.ValuesString":
-			v.Values = body.Values.Value.(servicebodyunion.ValuesString)
-		case "servicebodyunion.ValuesInt":
-			v.Values = body.Values.Value.(servicebodyunion.ValuesInt)
+		case "String":
+			var val servicebodyunion.ValuesString
+			json.Unmarshal([]byte(*body.Values.Value), &val)
+			v.Values = val
+		case "Int":
+			var val servicebodyunion.ValuesInt
+			json.Unmarshal([]byte(*body.Values.Value), &val)
+			v.Values = val
 		}
 	}