Repository: goadesign/goa
Pull request #3207 (2022-12-02 05:16:39)

Fix broken type references in custom package
Addresses #3198 

- Augmented the description of the `struct:pkg:path` Meta tag to include description of 2 two limitations
- Added validation to check for those two cases
- When that meta tag is set on a user type, internally copy that meta tag down the sub-type tree (for all user types that are fields within it and all user type fields within them)
- Changed one other unrelated Unions line to use existing `AddMeta()` function


Patch:
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index e6cae97b..0155ea70 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -12,7 +12,7 @@ jobs:
       if: success() || failure()
 
     - name: Set up Go 1.19
-      uses: actions/setup-go@v3.3.1
+      uses: actions/setup-go@v3.5.0
       with:
         go-version: 1.19
       id: go
@@ -56,7 +56,7 @@ jobs:
       if: success() || failure()
 
     - name: Set up Go 1.19
-      uses: actions/setup-go@v3.3.1
+      uses: actions/setup-go@v3.5.0
       with:
         go-version: 1.19
       id: go
diff --git a/.github/workflows/stale.yml b/.github/workflows/stale.yml
index 84b06d47..9042b4df 100644
--- a/.github/workflows/stale.yml
+++ b/.github/workflows/stale.yml
@@ -18,7 +18,7 @@ jobs:
       pull-requests: write
 
     steps:
-    - uses: actions/stale@v6
+    - uses: actions/stale@v7
       with:
         days-before-issue-stale: 60
         days-before-pr-stale: 90
diff --git a/Makefile b/Makefile
index 3ff51804..bdff24f9 100644
--- a/Makefile
+++ b/Makefile
@@ -13,8 +13,8 @@
 # - "all" is the default target, it runs "lint" and "test"
 #
 MAJOR=3
-MINOR=10
-BUILD=2
+MINOR=11
+BUILD=0
 
 GOOS=$(shell go env GOOS)
 GO_FILES=$(shell find . -type f -name '*.go')
diff --git a/README.md b/README.md
index f7fa06c8..5cb7fbea 100644
--- a/README.md
+++ b/README.md
@@ -2,7 +2,7 @@
 
 ## Overview
 
-[![GoDev](https://img.shields.io/badge/go-reference-007d9c?logo=go&logoColor=white&style=flat-square)](https://pkg.go.dev/goa.design/goa/v3@v3.10.2/dsl?tab=doc)
+[![GoDev](https://img.shields.io/badge/go-reference-007d9c?logo=go&logoColor=white&style=flat-square)](https://pkg.go.dev/goa.design/goa/v3@v3.11.0/dsl?tab=doc)
 [![Foresight](https://api-public.service.runforesight.com/api/v1/badge/test?repoId=67defc2e-b3f8-4043-bac7-a4e215518ed3)](http://goa.app.runforesight.com/)
 [![Go Report Card](https://goreportcard.com/badge/goadesign/goa)](https://goreportcard.com/report/goadesign/goa)
 [![Slack](https://img.shields.io/badge/Slack-gray.svg?longCache=true&logo=slack&colorB=red)](https://gophers.slack.com/messages/goa)
@@ -86,7 +86,7 @@ invoking the client code.
 go install goa.design/goa/v3/cmd/goa@v3
 ```
 
-Current Release: `v3.10.2`
+Current Release: `v3.11.0`
 
 ## Teaser
 
@@ -352,7 +352,7 @@ In particular the page
 explains how to leverage the generated code to implement an HTTP or gRPC
 service.
 
-The [![DSL GoDoc](https://img.shields.io/badge/godoc-DSL-blue)](https://pkg.go.dev/goa.design/goa/v3@v3.10.2/dsl?tab=doc)
+The [![DSL GoDoc](https://img.shields.io/badge/godoc-DSL-blue)](https://pkg.go.dev/goa.design/goa/v3@v3.11.0/dsl?tab=doc)
 contains a fully documented reference of all the DSL functions.
 
 ### Instrumentation and System Example
diff --git a/codegen/validation.go b/codegen/validation.go
index 719d6890..6f215571 100644
--- a/codegen/validation.go
+++ b/codegen/validation.go
@@ -152,11 +152,7 @@ func recurseValidationCode(att *expr.AttributeExpr, put expr.UserType, attCtx *A
 		// validating a proto-generated type since the HTTP
 		// serialization transforms unions into objects.
 		u := expr.AsUnion(att.Type)
-		tref := Goify(put.Name(), true)
-		if attCtx.DefaultPkg != "" {
-			tref = attCtx.DefaultPkg + "." + tref
-		}
-		tref = "*" + tref
+		tref := attCtx.Scope.Ref(&expr.AttributeExpr{Type: put}, attCtx.DefaultPkg)
 		var vals []string
 		var types []string
 		for _, v := range u.Values {
@@ -187,7 +183,21 @@ func recurseValidationCode(att *expr.AttributeExpr, put expr.UserType, attCtx *A
 func validateAttribute(ctx *AttributeContext, att *expr.AttributeExpr, put expr.UserType, target, context string, req bool) string {
 	ut, isUT := att.Type.(expr.UserType)
 	if !isUT {
-		return recurseValidationCode(att, put, ctx, req, false, target, context, nil).String()
+		code := recurseValidationCode(att, put, ctx, req, false, target, context, nil).String()
+		if code == "" {
+			return ""
+		}
+		if expr.IsArray(att.Type) || expr.IsMap(att.Type) || expr.IsUnion(att.Type) {
+			return code
+		}
+		if !ctx.Pointer && (req || (att.DefaultValue != nil && ctx.UseDefault)) {
+			return code
+		}
+		cond := fmt.Sprintf("if %s != nil {\n", target)
+		if strings.HasPrefix(code, cond) {
+			return code
+		}
+		return fmt.Sprintf("%s%s\n}", cond, code)
 	}
 	if expr.IsAlias(ut) {
 		return recurseValidationCode(ut.Attribute(), put, ctx, req, true, target, context, nil).String()
diff --git a/dsl/meta.go b/dsl/meta.go
index bc487b48..42c3b381 100644
--- a/dsl/meta.go
+++ b/dsl/meta.go
@@ -50,6 +50,13 @@ import (
 //	    Meta("struct:pkg:path", "types")
 //	})
 //
+// Note: If that meta tag is used more that once in the same design, but with
+// different values in the meta statement (ex. one type has Meta("struct:pkg:path", "types1")
+// and another has Meta("struct:pkg:path", "types2")) then those two types cannot
+// both contain a field of the same user type.
+// For the same reason, you may not set a different custom package in a user type than
+// the one set on a containing user type.
+//
 // - "struct:field:name" overrides the Go struct field name generated by default
 // by goa. Applicable to attributes only.
 //
diff --git a/dsl/method.go b/dsl/method.go
index 00606664..df000fca 100644
--- a/dsl/method.go
+++ b/dsl/method.go
@@ -25,6 +25,9 @@ import (
 //    })
 //
 func Method(name string, fn func()) {
+	if name == "" {
+		eval.ReportError("method name cannot be empty")
+	}
 	s, ok := eval.Current().(*expr.ServiceExpr)
 	if !ok {
 		eval.IncompatibleDSL()
diff --git a/dsl/result_type.go b/dsl/result_type.go
index 0842f23e..da573afc 100644
--- a/dsl/result_type.go
+++ b/dsl/result_type.go
@@ -281,12 +281,12 @@ func View(name string, adsl ...func()) {
 //	        Example("DivisionResult Collection Examples", func() {
 //	            Value([]Val{
 //	                {
-//	                    "value":    4.167,
-//	                    "reminder": 0,
+//	                    "value":     4.167,
+//	                    "remainder": 0,
 //	                },
 //	                {
-//	                    "value":    3.0,
-//	                    "reminder": 0,
+//	                    "value":     3.0,
+//	                    "remainder": 0,
 //	                },
 //	            })
 //	        })
diff --git a/expr/attribute.go b/expr/attribute.go
index 8cdab01b..5a26bac8 100644
--- a/expr/attribute.go
+++ b/expr/attribute.go
@@ -207,7 +207,24 @@ func (a *AttributeExpr) Validate(ctx string, parent eval.Expression) *eval.Valid
 				verr.Add(parent, `%srequired field %q does not exist in type %s`, ctx, n, a.Type.Name())
 			}
 		}
+		var pkgPath string
+		if ut, ok := a.Type.(UserType); ok {
+			if meta, ok := ut.Attribute().Meta["struct:pkg:path"]; ok {
+				pkgPath = meta[0]
+			}
+		}
 		for _, nat := range *o {
+			if ut, ok := nat.Attribute.Type.(UserType); pkgPath != "" && ok {
+				// This check ensures we error if a sub-type has a different custom package type set
+				// or if two user types have different custom packages but share a sub-type (field that's a user type)
+				if ut.Attribute().Meta != nil &&
+					ut.Attribute().Meta["struct:pkg:path"] != nil &&
+					ut.Attribute().Meta["struct:pkg:path"][0] != pkgPath {
+					verr.Add(a, "type \"%s\" has conflicting packages %s and %s", ut.Name(), ut.Attribute().Meta["struct:pkg:path"][0], pkgPath)
+				}
+
+				ut.Attribute().AddMeta("struct:pkg:path", pkgPath)
+			}
 			ctx = fmt.Sprintf("field %s", nat.Name)
 			verr.Merge(nat.Attribute.Validate(ctx, parent))
 		}
@@ -289,7 +306,7 @@ func (a *AttributeExpr) Finalize() {
 						// 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}
+							ut.Attribute().AddMeta("struct:pkg:path", pkgPath)
 						}
 					}
 				}
diff --git a/go.mod b/go.mod
index 1cc37f03..31a91840 100644
--- a/go.mod
+++ b/go.mod
@@ -5,17 +5,17 @@ go 1.19
 require (
 	github.com/dimfeld/httppath v0.0.0-20170720192232-ee938bf73598
 	github.com/dimfeld/httptreemux/v5 v5.5.0
-	github.com/getkin/kin-openapi v0.109.0
+	github.com/getkin/kin-openapi v0.112.0
 	github.com/google/uuid v1.3.0
 	github.com/gorilla/websocket v1.5.0
 	github.com/hashicorp/go-multierror v1.1.1
 	github.com/manveru/faker v0.0.0-20171103152722-9fbc68a78c4d
 	github.com/pkg/errors v0.9.1
-	github.com/sergi/go-diff v1.2.0
+	github.com/sergi/go-diff v1.3.1
 	github.com/zach-klippenstein/goregen v0.0.0-20160303162051-795b5e3961ea
-	golang.org/x/text v0.4.0
-	golang.org/x/tools v0.3.0
-	google.golang.org/grpc v1.51.0
+	golang.org/x/text v0.6.0
+	golang.org/x/tools v0.5.0
+	google.golang.org/grpc v1.52.3
 	google.golang.org/protobuf v1.28.1
 	gopkg.in/yaml.v3 v3.0.1
 )
@@ -33,9 +33,8 @@ require (
 	github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 // indirect
 	github.com/smartystreets/goconvey v1.7.2 // indirect
 	golang.org/x/mod v0.7.0 // indirect
-	golang.org/x/net v0.2.0 // indirect
-	golang.org/x/sys v0.2.0 // indirect
-	golang.org/x/xerrors v0.0.0-20220517211312-f3a8303e98df // indirect
-	google.golang.org/genproto v0.0.0-20220930163606-c98284e70a91 // indirect
+	golang.org/x/net v0.5.0 // indirect
+	golang.org/x/sys v0.4.0 // indirect
+	google.golang.org/genproto v0.0.0-20221118155620-16455021b5e6 // indirect
 	gopkg.in/yaml.v2 v2.4.0 // indirect
 )
diff --git a/go.sum b/go.sum
index 63f04b5f..144fb49b 100644
--- a/go.sum
+++ b/go.sum
@@ -6,8 +6,8 @@ github.com/dimfeld/httppath v0.0.0-20170720192232-ee938bf73598 h1:MGKhKyiYrvMDZs
 github.com/dimfeld/httppath v0.0.0-20170720192232-ee938bf73598/go.mod h1:0FpDmbrt36utu8jEmeU05dPC9AB5tsLYVVi+ZHfyuwI=
 github.com/dimfeld/httptreemux/v5 v5.5.0 h1:p8jkiMrCuZ0CmhwYLcbNbl7DDo21fozhKHQ2PccwOFQ=
 github.com/dimfeld/httptreemux/v5 v5.5.0/go.mod h1:QeEylH57C0v3VO0tkKraVz9oD3Uu93CKPnTLbsidvSw=
-github.com/getkin/kin-openapi v0.109.0 h1:Cpb0PmIPFEV0LVvikEvfo3gw3rBMVSjJ57w15j+/A/U=
-github.com/getkin/kin-openapi v0.109.0/go.mod h1:QtwUNt0PAAgIIBEvFWYfB7dfngxtAaqCX1zYHMZDeK8=
+github.com/getkin/kin-openapi v0.112.0 h1:lnLXx3bAG53EJVI4E/w0N8i1Y/vUZUEsnrXkgnfn7/Y=
+github.com/getkin/kin-openapi v0.112.0/go.mod h1:QtwUNt0PAAgIIBEvFWYfB7dfngxtAaqCX1zYHMZDeK8=
 github.com/go-openapi/jsonpointer v0.19.5 h1:gZr+CIYByUqjcgeLXnQu2gHYQC9o73G2XUeOFYEICuY=
 github.com/go-openapi/jsonpointer v0.19.5/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg=
 github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk=
@@ -17,7 +17,7 @@ github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaS
 github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw=
 github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
 github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
-github.com/google/go-cmp v0.5.6 h1:BKbKCqvP6I+rmFHt06ZmyQtvB8xAkWdhFyr0ZUNZcxQ=
+github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
 github.com/google/gxui v0.0.0-20151028112939-f85e0a97b3a4 h1:OL2d27ueTKnlQJoqLW2fc9pWYulFnJYLWzomGV7HqZo=
 github.com/google/gxui v0.0.0-20151028112939-f85e0a97b3a4/go.mod h1:Pw1H1OjSNHiqeuxAduB1BKYXIwFtsyrY47nEqSgEiCM=
 github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
@@ -59,8 +59,8 @@ github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
 github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
 github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
 github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
-github.com/sergi/go-diff v1.2.0 h1:XU+rvMAioB0UC3q1MFrIQy4Vo5/4VsRDQQXHsEya6xQ=
-github.com/sergi/go-diff v1.2.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM=
+github.com/sergi/go-diff v1.3.1 h1:xkr+Oxo4BOQKmkn/B9eMK0g5Kg/983T9DqqPHwYqD+8=
+github.com/sergi/go-diff v1.3.1/go.mod h1:aMJSSKb2lpPvRNec0+w3fl7LP9IOFzdc9Pa4NFbPK1I=
 github.com/smartystreets/assertions v1.2.0 h1:42S6lae5dvLc7BrLu/0ugRtcFVjoJNMC/N3yZFZkDFs=
 github.com/smartystreets/assertions v1.2.0/go.mod h1:tcbTF8ujkAEcZ8TElKY+i30BzYlVhC/LOxJk7iOWnoo=
 github.com/smartystreets/goconvey v1.7.2 h1:9RBaZCeXEQ3UselpuwUQHltGVXvdwm6cv1hgR6gDIPg=
@@ -81,25 +81,23 @@ golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACk
 golang.org/x/mod v0.7.0 h1:LapD9S96VoQRhi/GrNTqeBJFrUjs5UHCAtTlgwA5oZA=
 golang.org/x/mod v0.7.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
 golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
-golang.org/x/net v0.2.0 h1:sZfSu1wtKLGlWI4ZZayP0ck9Y73K1ynO6gqzTdBVdPU=
-golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY=
+golang.org/x/net v0.5.0 h1:GyT4nK/YDHSqa1c4753ouYCDajOYKTja9Xb/OHtgvSw=
+golang.org/x/net v0.5.0/go.mod h1:DivGGAXEgPSlEBzxGzZI+ZLohi+xUj054jfeKui00ws=
 golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o=
 golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
-golang.org/x/sys v0.2.0 h1:ljd4t30dBnAvMZaQCevtY0xLLD0A+bRZXbgLMLU1F/A=
-golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.4.0 h1:Zr2JFtRQNX3BCZ8YtxRE9hNJYC8J6I1MVbMg6owUp18=
+golang.org/x/sys v0.4.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
-golang.org/x/text v0.4.0 h1:BrVqGRd7+k1DiOgtnFvAkoQEWQvBc25ouMJM6429SFg=
-golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
+golang.org/x/text v0.6.0 h1:3XmdazWV+ubf7QgHSTWeykHOci5oeekaGJBLkrkaw4k=
+golang.org/x/text v0.6.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
 golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
-golang.org/x/tools v0.3.0 h1:SrNbZl6ECOS1qFzgTdQfWXZM9XBkiA6tkFrH9YSTPHM=
-golang.org/x/tools v0.3.0/go.mod h1:/rWhSS2+zyEVwoJf8YAX6L2f0ntZ7Kn/mGgAWcipA5k=
+golang.org/x/tools v0.5.0 h1:+bSpV5HIeWkuvgaMfI3UmKRThoTA5ODJTUd8T17NO+4=
+golang.org/x/tools v0.5.0/go.mod h1:N+Kgy78s5I24c24dU8OfWNEotWjutIs8SnJvn5IDq+k=
 golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
-golang.org/x/xerrors v0.0.0-20220517211312-f3a8303e98df h1:5Pf6pFKu98ODmgnpvkJ3kFUOQGGLIzLIkbzUHp47618=
-golang.org/x/xerrors v0.0.0-20220517211312-f3a8303e98df/go.mod h1:K8+ghG5WaK9qNqU5K3HdILfMLy1f3aNYFI/wnl100a8=
-google.golang.org/genproto v0.0.0-20220930163606-c98284e70a91 h1:Ezh2cpcnP5Rq60sLensUsFnxh7P6513NLvNtCm9iyJ4=
-google.golang.org/genproto v0.0.0-20220930163606-c98284e70a91/go.mod h1:3526vdqwhZAwq4wsRUaVG555sVgsNmIjRtO7t/JH29U=
-google.golang.org/grpc v1.51.0 h1:E1eGv1FTqoLIdnBCZufiSHgKjlqG6fKFf6pPWtMTh8U=
-google.golang.org/grpc v1.51.0/go.mod h1:wgNDFcnuBGmxLKI/qn4T+m5BtEBYXJPvibbUPsAIPww=
+google.golang.org/genproto v0.0.0-20221118155620-16455021b5e6 h1:a2S6M0+660BgMNl++4JPlcAO/CjkqYItDEZwkoDQK7c=
+google.golang.org/genproto v0.0.0-20221118155620-16455021b5e6/go.mod h1:rZS5c/ZVYMaOGBfO68GWtjOw/eLaZM1X6iVtgjZ+EWg=
+google.golang.org/grpc v1.52.3 h1:pf7sOysg4LdgBqduXveGKrcEwbStiK2rtfghdzlUYDQ=
+google.golang.org/grpc v1.52.3/go.mod h1:pu6fVzoFb+NBYNAvQL08ic+lvB2IojljRYuun5vorUY=
 google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
 google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
 google.golang.org/protobuf v1.28.1 h1:d0NfwRgPtno5B1Wa6L2DAG+KivqkdutMf1UhdNx175w=
@@ -110,7 +108,6 @@ gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8
 gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU=
 gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
 gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
-gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
 gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
 gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
 gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
diff --git a/http/client.go b/http/client.go
index e3f73d1e..aa2285c3 100644
--- a/http/client.go
+++ b/http/client.go
@@ -49,9 +49,15 @@ type (
 		Timeout bool
 		// Is the error a server-side fault?
 		Fault bool
+		// The original error if any
+		Err error
 	}
 )
 
+func (c ClientError) Unwrap() error {
+	return c.Err
+}
+
 // NewDebugDoer wraps the given doer and captures the request and response so
 // they can be printed.
 func NewDebugDoer(d Doer) DebugDoer {
@@ -157,28 +163,28 @@ func ErrInvalidType(svc, m, expected string, actual interface{}) error {
 // request body.
 func ErrEncodingError(svc, m string, err error) error {
 	msg := fmt.Sprintf("failed to encode request body: %s", err)
-	return &ClientError{Name: "encoding_error", Message: msg, Service: svc, Method: m}
+	return &ClientError{Name: "encoding_error", Message: msg, Service: svc, Method: m, Err: err}
 }
 
 // ErrInvalidURL is the error returned when the URL computed for an method is
 // invalid.
 func ErrInvalidURL(svc, m, u string, err error) error {
 	msg := fmt.Sprintf("invalid URL %s: %s", u, err)
-	return &ClientError{Name: "invalid_url", Message: msg, Service: svc, Method: m}
+	return &ClientError{Name: "invalid_url", Message: msg, Service: svc, Method: m, Err: err}
 }
 
 // ErrDecodingError is the error returned when the decoder fails to decode the
 // response body.
 func ErrDecodingError(svc, m string, err error) error {
 	msg := fmt.Sprintf("failed to decode response body: %s", err)
-	return &ClientError{Name: "decoding_error", Message: msg, Service: svc, Method: m}
+	return &ClientError{Name: "decoding_error", Message: msg, Service: svc, Method: m, Err: err}
 }
 
 // ErrValidationError is the error returned when the response body is properly
 // received and decoded but fails validation.
 func ErrValidationError(svc, m string, err error) error {
 	msg := fmt.Sprintf("invalid response: %s", err)
-	return &ClientError{Name: "validation_error", Message: msg, Service: svc, Method: m}
+	return &ClientError{Name: "validation_error", Message: msg, Service: svc, Method: m, Err: err}
 }
 
 // ErrInvalidResponse is the error returned when the service responded with an
@@ -214,5 +220,5 @@ func ErrRequestError(svc, m string, err error) error {
 		timeout = nerr.Timeout()
 	}
 	return &ClientError{Name: "request_error", Message: err.Error(), Service: svc, Method: m,
-		Temporary: temporary, Timeout: timeout}
+		Temporary: temporary, Timeout: timeout, Err: err}
 }
diff --git a/http/codegen/client.go b/http/codegen/client.go
index e4156122..ceb31c7c 100644
--- a/http/codegen/client.go
+++ b/http/codegen/client.go
@@ -868,7 +868,9 @@ const singleResponseT = ` {{- if .ClientBody }}
 				cookies = resp.Cookies()
 		{{- if not .ClientBody }}
 			{{- if .MustValidate }}
-				err error
+				{{- if not .Headers}}
+					err error
+				{{- end }}
 			{{- end }}
 		{{- end }}
 			)
diff --git a/http/codegen/service_data.go b/http/codegen/service_data.go
index 083892be..277712ce 100644
--- a/http/codegen/service_data.go
+++ b/http/codegen/service_data.go
@@ -2247,7 +2247,7 @@ func buildResponseBodyType(body, att *expr.AttributeExpr, loc *codegen.Location,
 				}
 				code, helpers, err = marshal(srcAtt, body, src, "body", svcctx, httpctx)
 				if err != nil {
-					fmt.Println(err.Error()) // TBD validate DSL so errors are not possible
+					panic(err) // bug
 				}
 				sd.ServerTransformHelpers = codegen.AppendHelpers(sd.ServerTransformHelpers, helpers)
 			}
diff --git a/pkg/version.go b/pkg/version.go
index 11e834aa..3f08c93a 100644
--- a/pkg/version.go
+++ b/pkg/version.go
@@ -10,9 +10,9 @@ const (
 	// Major version number
 	Major = 3
 	// Minor version number
-	Minor = 10
+	Minor = 11
 	// Build number
-	Build = 2
+	Build = 0
 	// Suffix - set to empty string in release tag commits.
 	Suffix = ""
 )


Test patch:
diff --git a/codegen/testdata/validation_code.go b/codegen/testdata/validation_code.go
index 757648dd..ccce3044 100644
--- a/codegen/testdata/validation_code.go
+++ b/codegen/testdata/validation_code.go
@@ -234,18 +234,12 @@ const (
 `
 
 	AliasTypeValidationCode = `func Validate() (err error) {
-	if target.RequiredAlias != nil {
-		err = goa.MergeErrors(err, goa.ValidatePattern("target.required_alias", string(*target.RequiredAlias), "^[A-z].*[a-z]$"))
+	err = goa.MergeErrors(err, goa.ValidatePattern("target.required_alias", string(target.RequiredAlias), "^[A-z].*[a-z]$"))
+	if utf8.RuneCountInString(string(target.RequiredAlias)) < 1 {
+		err = goa.MergeErrors(err, goa.InvalidLengthError("target.required_alias", string(target.RequiredAlias), utf8.RuneCountInString(string(target.RequiredAlias)), 1, true))
 	}
-	if target.RequiredAlias != nil {
-		if utf8.RuneCountInString(string(*target.RequiredAlias)) < 1 {
-			err = goa.MergeErrors(err, goa.InvalidLengthError("target.required_alias", string(*target.RequiredAlias), utf8.RuneCountInString(string(*target.RequiredAlias)), 1, true))
-		}
-	}
-	if target.RequiredAlias != nil {
-		if utf8.RuneCountInString(string(*target.RequiredAlias)) > 10 {
-			err = goa.MergeErrors(err, goa.InvalidLengthError("target.required_alias", string(*target.RequiredAlias), utf8.RuneCountInString(string(*target.RequiredAlias)), 10, false))
-		}
+	if utf8.RuneCountInString(string(target.RequiredAlias)) > 10 {
+		err = goa.MergeErrors(err, goa.InvalidLengthError("target.required_alias", string(target.RequiredAlias), utf8.RuneCountInString(string(target.RequiredAlias)), 10, false))
 	}
 	if target.Alias != nil {
 		err = goa.MergeErrors(err, goa.ValidatePattern("target.alias", string(*target.Alias), "^[A-z].*[a-z]$"))
@@ -527,5 +521,16 @@ const (
 		}
 	}
 }
+`
+
+	TypeWithEmbeddedTypeValidationCode = `func Validate() (err error) {
+	if target.Deep != nil {
+		if target.Deep.Integer != nil {
+			if err2 := ValidateInteger(target.Deep.Integer); err2 != nil {
+				err = goa.MergeErrors(err, err2)
+			}
+		}
+	}
+}
 `
 )
diff --git a/codegen/testdata/validation_types_dsl.go b/codegen/testdata/validation_types_dsl.go
index 4eddfd0d..92da6ce8 100644
--- a/codegen/testdata/validation_types_dsl.go
+++ b/codegen/testdata/validation_types_dsl.go
@@ -65,7 +65,7 @@ var ValidationTypesDSL = func() {
 		_ = Type("AliasType", func() {
 			Attribute("required_alias", Alias)
 			Attribute("alias", Alias)
-			Required("required_string")
+			Required("required_alias")
 		})
 
 		_ = Type("UserType", func() {
@@ -147,5 +147,11 @@ var ValidationTypesDSL = func() {
 				Attribute("collection", CollectionOf(Result))
 			})
 		})
+
+		_ = Type("Deep", func() {
+			Attribute("deep", func() {
+				Attribute("integer", IntegerT)
+			})
+		})
 	)
 }
diff --git a/codegen/validation_test.go b/codegen/validation_test.go
index a4e095c0..6823675b 100644
--- a/codegen/validation_test.go
+++ b/codegen/validation_test.go
@@ -24,6 +24,7 @@ func TestRecursiveValidationCode(t *testing.T) {
 		rtT      = root.UserType("Result")
 		rtcolT   = root.UserType("Collection")
 		colT     = root.UserType("TypeWithCollection")
+		deepT    = root.UserType("Deep")
 	)
 	cases := []struct {
 		Name       string
@@ -58,6 +59,7 @@ func TestRecursiveValidationCode(t *testing.T) {
 		{"collection-required", rtcolT, true, false, false, testdata.ResultCollectionPointerValidationCode},
 		{"collection-pointer", rtcolT, false, true, false, testdata.ResultCollectionPointerValidationCode},
 		{"type-with-collection-pointer", colT, false, true, false, testdata.TypeWithCollectionPointerValidationCode},
+		{"type-with-embedded-type", deepT, false, true, false, testdata.TypeWithEmbeddedTypeValidationCode},
 	}
 	for _, c := range cases {
 		t.Run(c.Name, func(t *testing.T) {
diff --git a/expr/attribute_test.go b/expr/attribute_test.go
index a8e33ed2..1d65a96a 100644
--- a/expr/attribute_test.go
+++ b/expr/attribute_test.go
@@ -124,6 +124,7 @@ func TestAttributeExprValidate(t *testing.T) {
 		errRequiredFieldNotExist = fmt.Errorf(`%srequired field %q does not exist in type %s`, normalizedCtx, "foo", fieldNotExistType.Name())
 		errViewButNotAResultType = fmt.Errorf("%s uses view %q but %q is not a result type", normalizedCtx, metadata["view"][0], notAResultType.Name())
 		errTypeNotDefineView     = fmt.Errorf("%s: type %q does not define view %q", normalizedCtx, viewNotDefinedTypeName, "foo")
+		errConflictingTypes      = fmt.Errorf("type \"%s\" has conflicting packages %s and %s", "SecondType", "types2", "types")
 	)
 	cases := map[string]struct {
 		typ        DataType
@@ -284,6 +285,124 @@ func TestAttributeExprValidate(t *testing.T) {
 			metadata: metadata,
 			expected: &eval.ValidationErrors{Errors: []error{errTypeNotDefineView}},
 		},
+		"custom package in parent type": {
+			typ: &UserTypeExpr{
+				TypeName: "FirstType",
+				AttributeExpr: &AttributeExpr{
+					Meta: MetaExpr{"struct:pkg:path": []string{"types"}},
+					Type: &Object{
+						&NamedAttributeExpr{
+							Name: "thing",
+							Attribute: &AttributeExpr{
+								Type: &UserTypeExpr{
+									AttributeExpr: &AttributeExpr{
+										Type: &Object{
+											&NamedAttributeExpr{
+												Name: "Description",
+												Attribute: &AttributeExpr{
+													Type: String,
+												},
+											},
+										},
+									},
+									TypeName: "SecondType",
+								},
+							},
+						},
+					},
+				},
+			},
+			expected: &eval.ValidationErrors{Errors: []error{}},
+		},
+		"custom packages in child type": {
+			typ: &UserTypeExpr{
+				TypeName: "FirstType",
+				AttributeExpr: &AttributeExpr{
+					Type: &Object{
+						&NamedAttributeExpr{
+							Name: "thing",
+							Attribute: &AttributeExpr{
+								Type: &UserTypeExpr{
+									AttributeExpr: &AttributeExpr{
+										Meta: MetaExpr{"struct:pkg:path": []string{"types"}},
+										Type: &Object{
+											&NamedAttributeExpr{
+												Name: "Description",
+												Attribute: &AttributeExpr{
+													Type: String,
+												},
+											},
+										},
+									},
+									TypeName: "SecondType",
+								},
+							},
+						},
+					},
+				},
+			},
+			expected: &eval.ValidationErrors{Errors: []error{}},
+		},
+		"matching custom packages between sub-type and parent": {
+			typ: &UserTypeExpr{
+				TypeName: "FirstType",
+				AttributeExpr: &AttributeExpr{
+					Meta: MetaExpr{"struct:pkg:path": []string{"types"}},
+					Type: &Object{
+						&NamedAttributeExpr{
+							Name: "thing",
+							Attribute: &AttributeExpr{
+								Type: &UserTypeExpr{
+									AttributeExpr: &AttributeExpr{
+										Meta: MetaExpr{"struct:pkg:path": []string{"types"}},
+										Type: &Object{
+											&NamedAttributeExpr{
+												Name: "Description",
+												Attribute: &AttributeExpr{
+													Type: String,
+												},
+											},
+										},
+									},
+									TypeName: "SecondType",
+								},
+							},
+						},
+					},
+				},
+			},
+			expected: &eval.ValidationErrors{Errors: []error{}},
+		},
+		"conflicting custom packages between sub-type and parent": {
+			typ: &UserTypeExpr{
+				TypeName: "FirstType",
+				AttributeExpr: &AttributeExpr{
+					Meta: MetaExpr{"struct:pkg:path": []string{"types"}},
+					Type: &Object{
+						&NamedAttributeExpr{
+							Name: "thing",
+							Attribute: &AttributeExpr{
+								Type: &UserTypeExpr{
+									AttributeExpr: &AttributeExpr{
+										Meta: MetaExpr{"struct:pkg:path": []string{"types2"}},
+										Type: &Object{
+											&NamedAttributeExpr{
+												Name: "Description",
+												Attribute: &AttributeExpr{
+													Type: String,
+												},
+											},
+										},
+									},
+									TypeName: "SecondType",
+								},
+							},
+						},
+					},
+				},
+			},
+			expected: &eval.ValidationErrors{Errors: []error{errConflictingTypes}},
+		},
 	}
 
 	for k, tc := range cases {
diff --git a/http/client_test.go b/http/client_test.go
new file mode 100644
index 00000000..29153cdf
--- /dev/null
+++ b/http/client_test.go
@@ -0,0 +1,55 @@
+package http
+
+import (
+	"errors"
+	"testing"
+)
+
+func TestClientError_Unwrap(t *testing.T) {
+	sentinelError := errors.New("this is na error")
+	alternateSentinelError := errors.New("another error")
+
+	tests := []struct {
+		name             string
+		err              error
+		checkedSentinel  error
+		expectedCausedBy bool
+	}{
+		{
+			name: "caused by sentinel",
+			err: ErrRequestError(
+				"AService",
+				"Something went wrong",
+				sentinelError,
+			),
+			checkedSentinel:  sentinelError,
+			expectedCausedBy: true,
+		},
+		{
+			name: "null cause hypothesis",
+			err: ErrRequestError(
+				"AService",
+				"Something went wrong",
+				sentinelError,
+			),
+			checkedSentinel:  alternateSentinelError,
+			expectedCausedBy: false,
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(
+			tt.name, func(t *testing.T) {
+				isCausedBy := errors.Is(tt.err, tt.checkedSentinel)
+
+				if isCausedBy != tt.expectedCausedBy {
+					if tt.expectedCausedBy {
+						t.Errorf("got error %#v, should be caused by %#v", tt.err, tt.checkedSentinel)
+					} else {
+						t.Errorf("got error %#v, must NOT be caused by %#v", tt.err, tt.checkedSentinel)
+					}
+				}
+			},
+		)
+	}
+}