Repository: goadesign/goa
Pull request #3100 (2022-07-20 04:35:47)

Fix handling of alias types and unions in gRPC marshalling functions
The previous code failed to cast aliased primitive types when assigning to the target struct fields. Also, in places it did cast it wouldn't take into consideration types defined in different packages.
Types with a `OneOf` attribute also had issues where the union value types would not be generated in the same package if the type had a `struct:pkg:path` Meta.  


Patch:
diff --git a/codegen/funcs.go b/codegen/funcs.go
index 673711a8..a33acfd9 100644
--- a/codegen/funcs.go
+++ b/codegen/funcs.go
@@ -274,7 +274,7 @@ func InitStructFields(args []*InitArgData, targetVar, sourcePkg, targetPkg strin
 		switch {
 		case arg.FieldName == "" && arg.FieldType == nil:
 		// do nothing
-		case expr.Equal(arg.Type, arg.FieldType):
+		case expr.Equal(unalias(arg.Type), arg.FieldType):
 			// arg type and struct field type are the same. No need to call transform
 			// to initialize the field
 			deref := ""
@@ -284,7 +284,11 @@ func InitStructFields(args []*InitArgData, targetVar, sourcePkg, targetPkg strin
 			code += fmt.Sprintf("%s.%s = %s%s\n", targetVar, arg.FieldName, deref, arg.Name)
 		case expr.IsPrimitive(arg.FieldType):
 			// aliased primitive type
-			t := scope.GoFullTypeRef(&expr.AttributeExpr{Type: arg.FieldType}, targetPkg)
+			pkg := targetPkg
+			if loc := UserTypeLocation(arg.FieldType); loc != nil {
+				pkg = loc.PackageName()
+			}
+			t := scope.GoFullTypeRef(&expr.AttributeExpr{Type: arg.FieldType}, pkg)
 			cast := fmt.Sprintf("%s(%s)", t, arg.Name)
 			if arg.Pointer {
 				code += "if " + arg.Name + " != nil {\n"
@@ -316,6 +320,18 @@ func InitStructFields(args []*InitArgData, targetVar, sourcePkg, targetPkg strin
 	return code, helpers, nil
 }
 
+// Get the underlying primitive type of a aliased type or return the type itself
+// if not aliased.
+func unalias(dt expr.DataType) expr.DataType {
+	if ut, ok := dt.(expr.UserType); ok {
+		if _, ok := ut.Attribute().Type.(expr.Primitive); ok {
+			return ut.Attribute().Type
+		}
+		return unalias(ut.Attribute().Type)
+	}
+	return dt
+}
+
 func runeSpacePosRev(r []rune) int {
 	for i := len(r) - 1; i > 0; i-- {
 		if unicode.IsSpace(r[i]) {
diff --git a/codegen/scope.go b/codegen/scope.go
index 1d0b3783..ac6cb097 100644
--- a/codegen/scope.go
+++ b/codegen/scope.go
@@ -234,11 +234,11 @@ func (s *NameScope) GoFullTypeName(att *expr.AttributeExpr, pkg string) string {
 		}
 		return GoNativeTypeName(actual)
 	case *expr.Array:
-		return "[]" + s.GoFullTypeRef(actual.ElemType, pkg)
+		return "[]" + s.GoFullTypeRef(actual.ElemType, pkgWithDefault(actual.ElemType.Type, pkg))
 	case *expr.Map:
 		return fmt.Sprintf("map[%s]%s",
-			s.GoFullTypeRef(actual.KeyType, pkg),
-			s.GoFullTypeRef(actual.ElemType, pkg))
+			s.GoFullTypeRef(actual.KeyType, pkgWithDefault(actual.KeyType.Type, pkg)),
+			s.GoFullTypeRef(actual.ElemType, pkgWithDefault(actual.ElemType.Type, pkg)))
 	case *expr.Object:
 		return s.GoTypeDef(att, false, false)
 	case expr.UserType, *expr.Union:
@@ -251,12 +251,22 @@ func (s *NameScope) GoFullTypeName(att *expr.AttributeExpr, pkg string) string {
 		}
 		return pkg + "." + n
 	case expr.CompositeExpr:
-		return s.GoFullTypeName(actual.Attribute(), pkg)
+		return s.GoFullTypeName(actual.Attribute(), pkgWithDefault(actual.Attribute().Type, pkg))
 	default:
 		panic(fmt.Sprintf("unknown data type %T", actual)) // bug
 	}
 }
 
+// pkgWithDefault returns the package defining the given type. If the types is a
+// user type with "struct:pkg:path" metadata then it returns the corresponding
+// value, otherwise it returns pkg.
+func pkgWithDefault(dt expr.DataType, pkg string) string {
+	if loc := UserTypeLocation(dt); loc != nil {
+		return loc.PackageName()
+	}
+	return pkg
+}
+
 func goTypeRef(name string, dt expr.DataType) string {
 	// For a raw struct, no need to dereference
 	if isRawStruct(dt) {
diff --git a/codegen/service/service_data.go b/codegen/service/service_data.go
index 63d4015f..1a1b3531 100644
--- a/codegen/service/service_data.go
+++ b/codegen/service/service_data.go
@@ -653,7 +653,7 @@ func (d ServicesData) analyze(service *expr.ServiceExpr) *Data {
 	{
 		methods = make([]*MethodData, len(service.Methods))
 		for i, e := range service.Methods {
-			m := buildMethodData(e, pkgName, service, scope)
+			m := buildMethodData(e, scope)
 			methods[i] = m
 			for _, s := range m.Schemes {
 				schemes = schemes.Append(s)
@@ -862,7 +862,7 @@ func buildErrorInitData(er *expr.ErrorExpr, scope *codegen.NameScope) *ErrorInit
 
 // buildMethodData creates the data needed to render the given endpoint. It
 // records the user types needed by the service definition in userTypes.
-func buildMethodData(m *expr.MethodExpr, svcPkgName string, service *expr.ServiceExpr, scope *codegen.NameScope) *MethodData {
+func buildMethodData(m *expr.MethodExpr, scope *codegen.NameScope) *MethodData {
 	var (
 		vname       string
 		desc        string
diff --git a/expr/attribute.go b/expr/attribute.go
index 004e41c6..75d638f8 100644
--- a/expr/attribute.go
+++ b/expr/attribute.go
@@ -249,10 +249,15 @@ func (a *AttributeExpr) Validate(ctx string, parent eval.Expression) *eval.Valid
 // Finalize merges base and reference type attributes and finalizes the Type
 // attribute.
 func (a *AttributeExpr) Finalize() {
+	if a.finalized {
+		return // Avoid infinite recursion.
+	}
+	a.finalized = true
 	if ut, ok := a.Type.(UserType); ok {
 		ut.Finalize()
 	}
-	if IsObject(a.Type) {
+	switch {
+	case IsObject(a.Type):
 		for _, ref := range a.References {
 			ru, ok := ref.(UserType)
 			if !ok {
@@ -267,14 +272,39 @@ func (a *AttributeExpr) Finalize() {
 			}
 			a.Merge(ru.Attribute())
 		}
-		if a.finalized {
-			// Avoid infinite recursion.
-			return
+		var pkgPath string
+		if ut, ok := a.Type.(UserType); ok {
+			if meta, ok := ut.Attribute().Meta["struct:pkg:path"]; ok {
+				pkgPath = meta[0]
+			}
 		}
-		a.finalized = true
 		for _, nat := range *AsObject(a.Type) {
+			if pkgPath != "" {
+				if u := AsUnion(nat.Attribute.Type); u != nil {
+					for _, nat := range u.Values {
+						// Union types are generated using a private interface
+						// to ensure that only types that are part of the enum
+						// can be assigned to the attribute. This means that the
+						// union values must be declared in the same package as
+						// the parent attribute.
+						if ut, ok := nat.Attribute.Type.(UserType); ok {
+							ut.Attribute().Meta["struct:pkg:path"] = []string{pkgPath}
+						}
+					}
+				}
+			}
+			nat.Attribute.Finalize()
+		}
+	case IsUnion(a.Type):
+		for _, nat := range AsUnion(a.Type).Values {
 			nat.Attribute.Finalize()
 		}
+	case IsArray(a.Type):
+		AsArray(a.Type).ElemType.Finalize()
+	case IsMap(a.Type):
+		m := AsMap(a.Type)
+		m.ElemType.Finalize()
+		m.KeyType.Finalize()
 	}
 }
 
diff --git a/expr/http_body_types.go b/expr/http_body_types.go
index 9afad4a2..8bb52e8e 100644
--- a/expr/http_body_types.go
+++ b/expr/http_body_types.go
@@ -92,7 +92,7 @@ func httpRequestBody(a *HTTPEndpointExpr) *AttributeExpr {
 	if !IsObject(payload.Type) {
 		if bodyOnly {
 			payload = DupAtt(payload)
-			removePkgPath(payload)
+			RemovePkgPath(payload)
 			renameType(payload, name, suffix)
 			return payload
 		}
@@ -101,7 +101,7 @@ func httpRequestBody(a *HTTPEndpointExpr) *AttributeExpr {
 
 	// 3. Remove header, param and cookies attributes
 	body := NewMappedAttributeExpr(payload)
-	removePkgPath(body.AttributeExpr)
+	RemovePkgPath(body.AttributeExpr)
 	extendBodyAttribute(body)
 	removeAttributes(body, headers)
 	removeAttributes(body, cookies)
@@ -224,14 +224,14 @@ func buildHTTPResponseBody(name string, attr *AttributeExpr, resp *HTTPResponseE
 	if !IsObject(attr.Type) {
 		if resp.Headers.IsEmpty() && resp.Cookies.IsEmpty() {
 			attr = DupAtt(attr)
-			removePkgPath(attr)
+			RemovePkgPath(attr)
 			renameType(attr, name, "Response") // Do not use ResponseBody as it could clash with name of element
 			return attr
 		}
 		return &AttributeExpr{Type: Empty}
 	}
 	body := NewMappedAttributeExpr(attr)
-	removePkgPath(body.AttributeExpr)
+	RemovePkgPath(body.AttributeExpr)
 	extendBodyAttribute(body)
 
 	// 4. Remove header and cookie attributes
@@ -398,15 +398,15 @@ func renameType(att *AttributeExpr, name, suffix string) {
 	}
 }
 
-// removePkgPath traverses the given data type and removes the "struct:pkg:path"
+// RemovePkgPath traverses the given data type and removes the "struct:pkg:path"
 // metadata from all the user type attributes.
-func removePkgPath(attr *AttributeExpr) {
+func RemovePkgPath(attr *AttributeExpr) {
 	walk(attr.Type, func(ut UserType) {
 		delete(ut.Attribute().Meta, "struct:pkg:path")
 	})
 	for _, pt := range attr.Bases {
 		if dt, ok := pt.(UserType); ok {
-			removePkgPath(dt.Attribute())
+			RemovePkgPath(dt.Attribute())
 		}
 	}
 }
diff --git a/grpc/codegen/client_cli.go b/grpc/codegen/client_cli.go
index f4fe82f6..27a2e41c 100644
--- a/grpc/codegen/client_cli.go
+++ b/grpc/codegen/client_cli.go
@@ -27,7 +27,7 @@ func ClientCLIFiles(genpkg string, root *expr.RootExpr) []*codegen.File {
 			sd := GRPCServices.Get(svc.Name())
 			command := cli.BuildCommandData(sd.Service)
 			for _, e := range sd.Endpoints {
-				flags, buildFunction := buildFlags(sd, e)
+				flags, buildFunction := buildFlags(e)
 				subcmd := cli.BuildSubcommandData(sd.Service.Name, e.Method, buildFunction, flags)
 				command.Subcommands = append(command.Subcommands, subcmd)
 			}
@@ -132,7 +132,7 @@ func payloadBuilders(genpkg string, svc *expr.GRPCServiceExpr, data *cli.Command
 	return &codegen.File{Path: fpath, SectionTemplates: sections}
 }
 
-func buildFlags(svc *ServiceData, e *EndpointData) ([]*cli.FlagData, *cli.BuildFunctionData) {
+func buildFlags(e *EndpointData) ([]*cli.FlagData, *cli.BuildFunctionData) {
 	if e.Request != nil {
 		return makeFlags(e, e.Request.CLIArgs)
 	}
diff --git a/grpc/codegen/client_types.go b/grpc/codegen/client_types.go
index d097d305..2b3963ea 100644
--- a/grpc/codegen/client_types.go
+++ b/grpc/codegen/client_types.go
@@ -92,6 +92,15 @@ func clientType(genpkg string, svc *expr.GRPCServiceExpr, seen map[string]struct
 				Name:   "client-type-init",
 				Source: typeInitT,
 				Data:   init,
+				FuncMap: map[string]interface{}{
+					"isAlias": expr.IsAlias,
+					"fullName": func(dt expr.DataType) string {
+						if loc := codegen.UserTypeLocation(dt); loc != nil {
+							return loc.PackageName() + "." + dt.Name()
+						}
+						return dt.Name()
+					},
+				},
 			})
 		}
 		for _, data := range sd.validations {
diff --git a/grpc/codegen/protobuf.go b/grpc/codegen/protobuf.go
index 4e575b0f..0a1e80a6 100644
--- a/grpc/codegen/protobuf.go
+++ b/grpc/codegen/protobuf.go
@@ -55,6 +55,7 @@ func protoBufTypeContext(pkg string, scope *codegen.NameScope) *codegen.Attribut
 // user type.
 func makeProtoBufMessage(att *expr.AttributeExpr, tname string, sd *ServiceData) *expr.AttributeExpr {
 	att = expr.DupAtt(att)
+	expr.RemovePkgPath(att)
 	ut, isut := att.Type.(expr.UserType)
 	switch {
 	case att.Type == expr.Empty:
diff --git a/grpc/codegen/protobuf_transform.go b/grpc/codegen/protobuf_transform.go
index 98123921..a3bbdfd1 100644
--- a/grpc/codegen/protobuf_transform.go
+++ b/grpc/codegen/protobuf_transform.go
@@ -168,9 +168,9 @@ func transformAttribute(source, target *expr.AttributeExpr, sourceVar, targetVar
 			code, err = transformObject(source, target, sourceVar, targetVar, newVar, ta)
 		case expr.IsUnion(source.Type):
 			if ta.proto {
-				code, err = transformUnionToProto(source, target, sourceVar, targetVar, newVar, ta)
+				code, err = transformUnionToProto(source, target, sourceVar, targetVar, ta)
 			} else {
-				code, err = transformUnionFromProto(source, target, sourceVar, targetVar, newVar, ta)
+				code, err = transformUnionFromProto(source, target, sourceVar, targetVar, ta)
 			}
 		default:
 			assign := "="
@@ -515,11 +515,11 @@ func transformMap(source, target *expr.Map, sourceVar, targetVar string, newVar
 // transformUnionToProto returns the code to transform an attribute of type
 // union from Goa to protobuf. It returns an error if source and target are not
 // compatible for transformation.
-func transformUnionToProto(source, target *expr.AttributeExpr, sourceVar, targetVar string, newVar bool, ta *transformAttrs) (string, error) {
+func transformUnionToProto(source, target *expr.AttributeExpr, sourceVar, targetVar string, ta *transformAttrs) (string, error) {
 	if err := codegen.IsCompatible(source.Type, target.Type, sourceVar, targetVar); err != nil {
 		return "", err
 	}
-	tdata := transformUnionData(source, target, sourceVar, targetVar, ta)
+	tdata := transformUnionData(source, target, ta)
 	targetValueTypeNames := make([]string, len(tdata.TargetValues))
 	targetFieldNames := make([]string, len(tdata.TargetValues))
 	for i, v := range tdata.TargetValues {
@@ -548,11 +548,11 @@ func transformUnionToProto(source, target *expr.AttributeExpr, sourceVar, target
 // transformUnionFromProto returns the code to transform an attribute of type
 // union from Goa to protobuf. It returns an error if source and target are not
 // compatible for transformation.
-func transformUnionFromProto(source, target *expr.AttributeExpr, sourceVar, targetVar string, newVar bool, ta *transformAttrs) (string, error) {
+func transformUnionFromProto(source, target *expr.AttributeExpr, sourceVar, targetVar string, ta *transformAttrs) (string, error) {
 	if err := codegen.IsCompatible(source.Type, target.Type, sourceVar, targetVar); err != nil {
 		return "", err
 	}
-	tdata := transformUnionData(source, target, sourceVar, targetVar, ta)
+	tdata := transformUnionData(source, target, ta)
 	sourceFieldNames := make([]string, len(tdata.SourceValues))
 	for i, v := range tdata.SourceValues {
 		fieldName := ta.SourceCtx.Scope.Field(v.Attribute, v.Name, true)
@@ -642,7 +642,7 @@ func checkZeroValue(dt expr.DataType, target string, negate bool) string {
 }
 
 // transformUnionData returns data needed by both transformUnion functions.
-func transformUnionData(source, target *expr.AttributeExpr, sourceVar, targetVar string, ta *transformAttrs) *unionData {
+func transformUnionData(source, target *expr.AttributeExpr, ta *transformAttrs) *unionData {
 	src := expr.AsUnion(source.Type)
 	tgt := expr.AsUnion(target.Type)
 	srcValues := make([]*expr.NamedAttributeExpr, len(src.Values))
@@ -724,7 +724,7 @@ func transformAttributeHelpers(source, target *expr.AttributeExpr, ta *transform
 				}
 			}
 		case expr.IsObject(source.Type):
-			walkMatches(source, target, func(srcMatt, tgtMatt *expr.MappedAttributeExpr, srcc, tgtc *expr.AttributeExpr, n string) {
+			walkMatches(source, target, func(srcMatt, _ *expr.MappedAttributeExpr, srcc, tgtc *expr.AttributeExpr, n string) {
 				if err != nil {
 					return
 				}
diff --git a/grpc/codegen/server.go b/grpc/codegen/server.go
index 26b6e4d0..7f39ed92 100644
--- a/grpc/codegen/server.go
+++ b/grpc/codegen/server.go
@@ -295,7 +295,7 @@ func Decode{{ .Method.VarName }}Request(ctx context.Context, v interface{}, md m
 	)
 	{
 	{{- range .Request.Metadata }}
-		{{- if or (eq .Type.Name "string") (eq .Type.Name "any") }}
+		{{- if or (eq .TypeName "string") (eq .Type.Name "any") }}
 			{{- if .Required }}
 				if vals := md.Get({{ printf "%q" .Name }}); len(vals) == 0 {
 					err = goa.MergeErrors(err, goa.MissingFieldError({{ printf "%q" .Name }}, "metadata"))
@@ -436,10 +436,10 @@ func Encode{{ .Method.VarName }}Response(ctx context.Context, v interface{}, hdr
 		{{- end }}
 		{{ .VarName }}.Append({{ printf "%q" .Metadata.Name }},
 			{{- if eq .Metadata.Type.Name "bytes" }} string(
-			{{- else if not (eq .Metadata.Type.Name "string") }} fmt.Sprintf("%v",
+			{{- else if not (eq .Metadata.TypeName "string") }} fmt.Sprintf("%v",
 			{{- end }}
 			{{- if .Metadata.Pointer }}*{{ end }}p.{{ .Metadata.FieldName }}
-			{{- if or (eq .Metadata.Type.Name "bytes") (not (eq .Metadata.Type.Name "string")) }})
+			{{- if or (eq .Metadata.Type.Name "bytes") (not (eq .Metadata.TypeName "string")) }})
 			{{- end }})
 		{{- if .Metadata.Pointer }}
 			}
diff --git a/grpc/codegen/server_types.go b/grpc/codegen/server_types.go
index 89b910e8..1152b4cf 100644
--- a/grpc/codegen/server_types.go
+++ b/grpc/codegen/server_types.go
@@ -90,6 +90,15 @@ func serverType(genpkg string, svc *expr.GRPCServiceExpr, seen map[string]struct
 				Name:   "server-type-init",
 				Source: typeInitT,
 				Data:   init,
+				FuncMap: map[string]interface{}{
+					"isAlias": expr.IsAlias,
+					"fullName": func(dt expr.DataType) string {
+						if loc := codegen.UserTypeLocation(dt); loc != nil {
+							return loc.PackageName() + "." + dt.Name()
+						}
+						return dt.Name()
+					},
+				},
 			})
 			foundInits[init.Name] = struct{}{}
 		}
diff --git a/grpc/codegen/service_data.go b/grpc/codegen/service_data.go
index 108b6d0c..2ae43420 100644
--- a/grpc/codegen/service_data.go
+++ b/grpc/codegen/service_data.go
@@ -1256,7 +1256,7 @@ func extractMetadata(a *expr.MappedAttributeExpr, service *expr.AttributeExpr, s
 
 			arr     = expr.AsArray(c.Type)
 			mp      = expr.AsMap(c.Type)
-			typeRef = scope.GoTypeRef(c)
+			typeRef = scope.GoTypeRef(unalias(c))
 			ft      = service.Type
 		)
 		{
@@ -1281,7 +1281,7 @@ func extractMetadata(a *expr.MappedAttributeExpr, service *expr.AttributeExpr, s
 			VarName:       varn,
 			Required:      required,
 			Type:          c.Type,
-			TypeName:      scope.GoTypeName(c),
+			TypeName:      scope.GoTypeName(unalias(c)),
 			TypeRef:       typeRef,
 			Pointer:       pointer,
 			Slice:         arr != nil,
@@ -1291,7 +1291,7 @@ func extractMetadata(a *expr.MappedAttributeExpr, service *expr.AttributeExpr, s
 				mp.KeyType.Type.Kind() == expr.StringKind &&
 				mp.ElemType.Type.Kind() == expr.ArrayKind &&
 				expr.AsArray(mp.ElemType.Type).ElemType.Type.Kind() == expr.StringKind,
-			Validate:     codegen.RecursiveValidationCode(c, ctx, required, expr.IsAlias(c.Type), varn),
+			Validate:     codegen.RecursiveValidationCode(c, ctx, required, false, varn),
 			DefaultValue: c.DefaultValue,
 			Example:      c.Example(expr.Root.API.Random()),
 		})
@@ -1300,6 +1300,16 @@ func extractMetadata(a *expr.MappedAttributeExpr, service *expr.AttributeExpr, s
 	return metadata
 }
 
+func unalias(att *expr.AttributeExpr) *expr.AttributeExpr {
+	if ut, ok := att.Type.(expr.UserType); ok {
+		if _, ok := ut.Attribute().Type.(expr.Primitive); ok {
+			return ut.Attribute()
+		}
+		return unalias(ut.Attribute())
+	}
+	return att
+}
+
 // serviceTypeContext returns a contextual attribute for service types. Service
 // types are Go types and uses non-pointers to hold attributes having default
 // values.
@@ -1358,7 +1368,7 @@ func {{ .Name }}({{ range .Args }}{{ .Name }} {{ .TypeRef }}, {{ end }}) {{ .Ret
 {{- if .ReturnIsStruct }}
 	{{- range .Args }}
 		{{- if .FieldName }}
-			{{ $.ReturnVarName }}.{{ .FieldName }} = {{ .Name }}
+			{{ $.ReturnVarName }}.{{ .FieldName }} = {{ if isAlias .FieldType }}{{ fullName .FieldType }}({{ end }}{{ .Name }}{{ if isAlias .FieldType }}){{ end }}
 		{{- end }}
 	{{- end }}
 {{- end }}
diff --git a/http/codegen/client.go b/http/codegen/client.go
index 436e37ee..6e82354a 100644
--- a/http/codegen/client.go
+++ b/http/codegen/client.go
@@ -440,10 +440,10 @@ func {{ .RequestEncoder }}(encoder func(*http.Request) goahttp.Encoder) func(*ht
 				req.Header.Add({{ printf "%q" .Name }}, valStr)
 				{{- end }}
 			}
+			{{- else if (and (isAlias .FieldType) (eq (underlyingType .FieldType).Name "string")) }}
+			req.Header.Set({{ printf "%q" .Name }}, string(head))
 			{{- else if eq .Type.Name "string" }}
 			req.Header.Set({{ printf "%q" .Name }}, head)
-			{{- else if (and (isAlias .Type) (eq (underlyingType .Type).Name "string")) }}
-			req.Header.Set({{ printf "%q" .Name }}, string(head))
 			{{- else }}
 			{{ template "type_conversion" (typeConversionData .Type .FieldType "headStr" "head") }}
 			req.Header.Set({{ printf "%q" .Name }}, headStr)
diff --git a/http/codegen/service_data.go b/http/codegen/service_data.go
index 4738ebf3..9b7f469a 100644
--- a/http/codegen/service_data.go
+++ b/http/codegen/service_data.go
@@ -2422,13 +2422,14 @@ func extractQueryParams(a *expr.MappedAttributeExpr, service *expr.AttributeExpr
 func extractHeaders(a *expr.MappedAttributeExpr, svcAtt *expr.AttributeExpr, svcCtx *codegen.AttributeContext, scope *codegen.NameScope) []*HeaderData {
 	var headers []*HeaderData
 	codegen.WalkMappedAttr(a, func(name, elem string, required bool, _ *expr.AttributeExpr) error {
+		var attr *expr.AttributeExpr
+		if attr = svcAtt.Find(name); attr == nil {
+			attr = svcAtt
+		}
 		var hattr *expr.AttributeExpr
 		var stringSlice bool
 		{
-			if hattr = svcAtt.Find(name); hattr == nil {
-				hattr = svcAtt
-			}
-			hattr = expr.DupAtt(hattr)
+			hattr = expr.DupAtt(attr)
 
 			// The StringSlice field of ParamData must be false for aliased primitive types
 			if arr := expr.AsArray(hattr.Type); arr != nil {
@@ -2441,7 +2442,7 @@ func extractHeaders(a *expr.MappedAttributeExpr, svcAtt *expr.AttributeExpr, svc
 			varn    = scope.Name(codegen.Goify(name, false))
 			arr     = expr.AsArray(hattr.Type)
 			typeRef = scope.GoTypeRef(hattr)
-			ft      = svcAtt.Type
+			ft      = attr.Type
 
 			fieldName string
 			pointer   bool
@@ -2452,7 +2453,6 @@ func extractHeaders(a *expr.MappedAttributeExpr, svcAtt *expr.AttributeExpr, svc
 			if expr.IsObject(svcAtt.Type) {
 				fieldName = codegen.Goify(name, true)
 				fptr = svcCtx.IsPrimitivePointer(name, svcAtt)
-				ft = svcAtt.Find(name).Type
 			}
 			if pointer {
 				typeRef = "*" + typeRef


Test patch:
diff --git a/codegen/service/service_test.go b/codegen/service/service_test.go
index e6943550..902e67d2 100644
--- a/codegen/service/service_test.go
+++ b/codegen/service/service_test.go
@@ -81,6 +81,7 @@ func TestStructPkgPath(t *testing.T) {
 	}{
 		{"none", testdata.SingleMethodDSL, []string{testdata.SingleMethod}, nil, nil},
 		{"single", testdata.PkgPathDSL, []string{testdata.PkgPath}, []string{fooPath}, []string{testdata.PkgPathFoo}},
+		{"array", testdata.PkgPathArrayDSL, []string{testdata.PkgPathArray}, []string{fooPath}, []string{testdata.PkgPathArrayFoo}},
 		{"recursive", testdata.PkgPathRecursiveDSL, []string{testdata.PkgPathRecursive}, []string{fooPath, recursiveFooPath}, []string{testdata.PkgPathRecursiveFooFoo, testdata.PkgPathRecursiveFoo}},
 		{"multiple", testdata.PkgPathMultipleDSL, []string{testdata.PkgPathMultiple}, []string{barPath, bazPath}, []string{testdata.PkgPathBar, testdata.PkgPathBaz}},
 		{"nopkg", testdata.PkgPathNoDirDSL, []string{testdata.PkgPathNoDir}, nil, nil},
diff --git a/codegen/service/testdata/service_code.go b/codegen/service/testdata/service_code.go
index 80d5c921..71311517 100644
--- a/codegen/service/testdata/service_code.go
+++ b/codegen/service/testdata/service_code.go
@@ -2462,6 +2462,24 @@ const ServiceName = "PkgPathMethod"
 var MethodNames = [1]string{"A"}
 `
 
+const PkgPathArray = `
+// Service is the PkgPathArrayMethod service interface.
+type Service interface {
+	// A implements A.
+	A(context.Context, []*foo.Foo) (res []*foo.Foo, err error)
+}
+
+// ServiceName is the name of the service as defined in the design. This is the
+// same value that is set in the endpoint request contexts under the ServiceKey
+// key.
+const ServiceName = "PkgPathArrayMethod"
+
+// MethodNames lists the service method names as defined in the design. These
+// are the same values that are set in the endpoint request contexts under the
+// MethodKey key.
+var MethodNames = [1]string{"A"}
+`
+
 const PkgPathRecursive = `
 // Service is the PkgPathRecursiveMethod service interface.
 type Service interface {
@@ -2520,6 +2538,12 @@ type Foo struct {
 }
 `
 
+const PkgPathArrayFoo = `
+type Foo struct {
+	IntField *int
+}
+`
+
 const PkgPathRecursiveFooFoo = `
 type Foo struct {
 	IntField *int
diff --git a/codegen/service/testdata/service_dsls.go b/codegen/service/testdata/service_dsls.go
index 7236506f..5015eac0 100644
--- a/codegen/service/testdata/service_dsls.go
+++ b/codegen/service/testdata/service_dsls.go
@@ -808,6 +808,19 @@ var PkgPathDSL = func() {
 	})
 }
 
+var PkgPathArrayDSL = func() {
+	var Foo = Type("Foo", func() {
+		Attribute("IntField", Int)
+		Meta("struct:pkg:path", "foo")
+	})
+	Service("PkgPathArrayMethod", func() {
+		Method("A", func() {
+			Payload(ArrayOf(Foo))
+			Result(ArrayOf(Foo))
+		})
+	})
+}
+
 var PkgPathRecursiveDSL = func() {
 	var Foo = Type("Foo", func() {
 		Attribute("IntField", Int)