Repository: goadesign/goa
Pull request #3164 (2022-10-11 17:53:43)

Fix issue with union validations
Refactor validation code to remove duplication of logic.


Patch:
diff --git a/codegen/service/service_data.go b/codegen/service/service_data.go
index 8b670e09..2dbd3257 100644
--- a/codegen/service/service_data.go
+++ b/codegen/service/service_data.go
@@ -1662,7 +1662,7 @@ func buildValidations(projected *expr.AttributeExpr, scope *codegen.NameScope) [
 					})
 					ctx = projectedTypeContext("", !expr.IsPrimitive(projected.Type), scope)
 				}
-				data["Validate"] = codegen.ValidationCode(&expr.AttributeExpr{Type: o, Validation: rt.Validation}, ctx, true, false, "result")
+				data["Validate"] = codegen.ValidationCode(&expr.AttributeExpr{Type: o, Validation: rt.Validation}, rt, ctx, true, false, "result")
 				data["Fields"] = fields
 			}
 
@@ -1687,7 +1687,7 @@ func buildValidations(projected *expr.AttributeExpr, scope *codegen.NameScope) [
 			Name:        name,
 			Description: fmt.Sprintf("%s runs the validations defined on %s.", name, tname),
 			Ref:         scope.GoTypeRef(projected),
-			Validate:    codegen.ValidationCode(ut.Attribute(), ctx, true, expr.IsAlias(ut), "result"),
+			Validate:    codegen.ValidationCode(ut.Attribute(), ut, ctx, true, expr.IsAlias(ut), "result"),
 		})
 	}
 	return validations
diff --git a/codegen/validation.go b/codegen/validation.go
index 1a26a5d0..60480da8 100644
--- a/codegen/validation.go
+++ b/codegen/validation.go
@@ -20,6 +20,7 @@ var (
 	requiredValT   *template.Template
 	arrayValT      *template.Template
 	mapValT        *template.Template
+	unionValT      *template.Template
 	userValT       *template.Template
 )
 
@@ -29,7 +30,6 @@ func init() {
 		"oneof":    oneof,
 		"constant": constant,
 		"add":      func(a, b int) int { return a + b },
-		"isset":    func(i interface{}) bool { return i != nil },
 	}
 	enumValT = template.Must(template.New("enum").Funcs(fm).Parse(enumValTmpl))
 	formatValT = template.Must(template.New("format").Funcs(fm).Parse(formatValTmpl))
@@ -40,6 +40,7 @@ func init() {
 	requiredValT = template.Must(template.New("req").Funcs(fm).Parse(requiredValTmpl))
 	arrayValT = template.Must(template.New("array").Funcs(fm).Parse(arrayValTmpl))
 	mapValT = template.Must(template.New("map").Funcs(fm).Parse(mapValTmpl))
+	unionValT = template.Must(template.New("union").Funcs(fm).Parse(unionValTmpl))
 	userValT = template.Must(template.New("user").Funcs(fm).Parse(userValTmpl))
 }
 
@@ -47,6 +48,8 @@ func init() {
 // given attribute and its children recursively against the value held by the
 // variable named target.
 //
+// put is the parent UserType if any. It is used to compute proto oneof type names.
+//
 // attCtx is the attribute context used to generate attribute name and reference
 // in the validation code.
 //
@@ -57,12 +60,12 @@ func init() {
 // target is the variable name against which the validation code is generated
 //
 // context is used to produce helpful messages in case of error.
-func ValidationCode(att *expr.AttributeExpr, attCtx *AttributeContext, req, alias bool, target string) string {
+func ValidationCode(att *expr.AttributeExpr, put expr.UserType, attCtx *AttributeContext, req, alias bool, target string) string {
 	seen := make(map[string]*bytes.Buffer)
-	return recurseValidationCode(att, attCtx, req, alias, target, target, seen).String()
+	return recurseValidationCode(att, put, attCtx, req, alias, target, target, seen).String()
 }
 
-func recurseValidationCode(att *expr.AttributeExpr, attCtx *AttributeContext, req, alias bool, target, context string, seen map[string]*bytes.Buffer) *bytes.Buffer {
+func recurseValidationCode(att *expr.AttributeExpr, put expr.UserType, attCtx *AttributeContext, req, alias bool, target, context string, seen map[string]*bytes.Buffer) *bytes.Buffer {
 	var (
 		buf      = new(bytes.Buffer)
 		first    = true
@@ -79,26 +82,6 @@ func recurseValidationCode(att *expr.AttributeExpr, attCtx *AttributeContext, re
 
 	flattenValidations(att, make(map[string]struct{}))
 
-	code := func(ctx *AttributeContext, att *expr.AttributeExpr, tgt, suf string) string {
-		ut, isUT = att.Type.(expr.UserType)
-		if !isUT {
-			return recurseValidationCode(att, ctx, true, false, tgt, context+suf, seen).String()
-		}
-		if expr.IsAlias(ut) {
-			return recurseValidationCode(ut.Attribute(), ctx, true, true, tgt, context+suf, seen).String()
-		}
-		if !hasValidations(attCtx, ut) {
-			return ""
-		}
-		var buf bytes.Buffer
-		name := attCtx.Scope.Name(att, ctx.Pkg(att), ctx.Pointer, ctx.UseDefault)
-		data := map[string]interface{}{"name": Goify(name, true), "target": tgt}
-		if err := userValT.Execute(&buf, data); err != nil {
-			panic(err) // bug
-		}
-		return fmt.Sprintf("if %s != nil {\n\t%s\n}", tgt, buf.String())
-	}
-
 	newline := func() {
 		if !first {
 			buf.WriteByte('\n')
@@ -117,11 +100,16 @@ func recurseValidationCode(att *expr.AttributeExpr, attCtx *AttributeContext, re
 	// Recurse down depending on attribute type.
 	switch {
 	case expr.IsObject(att.Type):
+		if isUT {
+			put = ut
+		}
 		for _, nat := range *(expr.AsObject(att.Type)) {
-			validation := recurseAttribute(att, attCtx, nat, target, context, seen)
-			if validation != "" {
+			tgt := fmt.Sprintf("%s.%s", target, attCtx.Scope.Field(nat.Attribute, nat.Name, true))
+			ctx := fmt.Sprintf("%s.%s", context, nat.Name)
+			val := validateAttribute(attCtx, nat.Attribute, put, tgt, ctx, att.IsRequired(nat.Name))
+			if val != "" {
 				newline()
-				buf.WriteString(validation)
+				buf.WriteString(val)
 			}
 		}
 	case expr.IsArray(att.Type):
@@ -132,7 +120,7 @@ func recurseValidationCode(att *expr.AttributeExpr, attCtx *AttributeContext, re
 			ctx = attCtx.Dup()
 			ctx.Pointer = false
 		}
-		val := code(ctx, elem, "e", "[*]")
+		val := validateAttribute(ctx, elem, put, "e", context+"[*]", true)
 		if val != "" {
 			newline()
 			data := map[string]interface{}{"target": target, "validation": val}
@@ -144,11 +132,11 @@ func recurseValidationCode(att *expr.AttributeExpr, attCtx *AttributeContext, re
 		m := expr.AsMap(att.Type)
 		ctx := attCtx.Dup()
 		ctx.Pointer = false
-		keyVal := code(ctx, m.KeyType, "k", ".key")
+		keyVal := validateAttribute(ctx, m.KeyType, put, "k", context+".key", true)
 		if keyVal != "" {
 			keyVal = "\n" + keyVal
 		}
-		valueVal := code(ctx, m.ElemType, "v", "[key]")
+		valueVal := validateAttribute(ctx, m.ElemType, put, "v", context+"[key]", true)
 		if valueVal != "" {
 			valueVal = "\n" + valueVal
 		}
@@ -159,76 +147,61 @@ func recurseValidationCode(att *expr.AttributeExpr, attCtx *AttributeContext, re
 				panic(err) // bug
 			}
 		}
-	}
-
-	return buf
-}
-
-func flattenValidations(att *expr.AttributeExpr, seen map[string]struct{}) {
-	switch actual := att.Type.(type) {
-	case *expr.Array:
-		flattenValidations(actual.ElemType, seen)
-	case *expr.Map:
-		flattenValidations(actual.KeyType, seen)
-		flattenValidations(actual.ElemType, seen)
-	case *expr.Object:
-		for _, nat := range *actual {
-			flattenValidations(nat.Attribute, seen)
-		}
-	case *expr.Union:
-		for _, nat := range actual.Values {
-			flattenValidations(nat.Attribute, seen)
-		}
-	case expr.UserType:
-		if _, ok := seen[actual.ID()]; ok {
-			return
+	case expr.IsUnion(att.Type):
+		// NOTE: the only time we validate a union is when we are
+		// 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
+		var vals []string
+		var types []string
+		for _, v := range u.Values {
+			vatt := v.Attribute
+			fieldName := attCtx.Scope.Field(vatt, v.Name, true)
+			val := validateAttribute(attCtx, vatt, put, "v."+fieldName, context+".value", true)
+			if val != "" {
+				types = append(types, tref+"_"+fieldName)
+				vals = append(vals, val)
+			}
 		}
-		seen[actual.ID()] = struct{}{}
-		v := att.Validation
-		ut, ok := actual.Attribute().Type.(expr.UserType)
-		for ok {
-			if val := ut.Attribute().Validation; val != nil {
-				if v == nil {
-					v = val
-				} else {
-					v.Merge(val)
-				}
+		if len(vals) > 0 {
+			newline()
+			data := map[string]interface{}{
+				"target": target,
+				"types":  types,
+				"values": vals,
+			}
+			if err := unionValT.Execute(buf, data); err != nil {
+				panic(err) // bug
 			}
-			ut, ok = ut.Attribute().Type.(expr.UserType)
 		}
-		att.Validation = v
-		flattenValidations(actual.Attribute(), seen)
 	}
+
+	return buf
 }
 
-func recurseAttribute(att *expr.AttributeExpr, attCtx *AttributeContext, nat *expr.NamedAttributeExpr, target, context string, seen map[string]*bytes.Buffer) string {
-	var validation string
-	ut, isut := nat.Attribute.Type.(expr.UserType)
-	if !isut {
-		target := fmt.Sprintf("%s.%s", target, attCtx.Scope.Field(nat.Attribute, nat.Name, true))
-		context := fmt.Sprintf("%s.%s", context, nat.Name)
-		code := recurseValidationCode(nat.Attribute, attCtx, att.IsRequired(nat.Name), false, target, context, seen)
-		validation = code.String()
-	} else if hasValidations(attCtx, ut) {
-		var buf bytes.Buffer
-		tgt := fmt.Sprintf("%s.%s", target, attCtx.Scope.Field(nat.Attribute, nat.Name, true))
-		if expr.IsPrimitive(nat.Attribute.Type) {
-			buf.Write(recurseValidationCode(ut.Attribute(), attCtx, att.IsRequired(nat.Name), true, tgt, context, seen).Bytes())
-		} else {
-			name := attCtx.Scope.Name(nat.Attribute, attCtx.Pkg(nat.Attribute), attCtx.Pointer, attCtx.UseDefault)
-			if err := userValT.Execute(&buf, map[string]interface{}{"name": Goify(name, true), "target": tgt}); err != nil {
-				panic(err) // bug
-			}
-		}
-		validation = buf.String()
+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()
 	}
-	if validation != "" {
-		if expr.IsObject(nat.Attribute.Type) {
-			validation = fmt.Sprintf("if %s.%s != nil {\n%s\n}",
-				target, attCtx.Scope.Field(nat.Attribute, nat.Name, true), validation)
-		}
+	if expr.IsAlias(ut) {
+		return recurseValidationCode(ut.Attribute(), put, ctx, req, true, target, context, nil).String()
+	}
+	if !hasValidations(ctx, ut) {
+		return ""
 	}
-	return validation
+	var buf bytes.Buffer
+	name := ctx.Scope.Name(att, "", ctx.Pointer, ctx.UseDefault)
+	data := map[string]interface{}{"name": Goify(name, true), "target": target}
+	if err := userValT.Execute(&buf, data); err != nil {
+		panic(err) // bug
+	}
+	return fmt.Sprintf("if %s != nil {\n\t%s\n}", target, buf.String())
 }
 
 // validationCode produces Go code that runs the validations defined in the
@@ -420,6 +393,43 @@ func generatedRequiredValidation(att *expr.AttributeExpr, attCtx *AttributeConte
 	return
 }
 
+func flattenValidations(att *expr.AttributeExpr, seen map[string]struct{}) {
+	switch actual := att.Type.(type) {
+	case *expr.Array:
+		flattenValidations(actual.ElemType, seen)
+	case *expr.Map:
+		flattenValidations(actual.KeyType, seen)
+		flattenValidations(actual.ElemType, seen)
+	case *expr.Object:
+		for _, nat := range *actual {
+			flattenValidations(nat.Attribute, seen)
+		}
+	case *expr.Union:
+		for _, nat := range actual.Values {
+			flattenValidations(nat.Attribute, seen)
+		}
+	case expr.UserType:
+		if _, ok := seen[actual.ID()]; ok {
+			return
+		}
+		seen[actual.ID()] = struct{}{}
+		v := att.Validation
+		ut, ok := actual.Attribute().Type.(expr.UserType)
+		for ok {
+			if val := ut.Attribute().Validation; val != nil {
+				if v == nil {
+					v = val
+				} else {
+					v.Merge(val)
+				}
+			}
+			ut, ok = ut.Attribute().Type.(expr.UserType)
+		}
+		att.Validation = v
+		flattenValidations(actual.Attribute(), seen)
+	}
+}
+
 // toSlice returns Go code that represents the given slice.
 func toSlice(val []interface{}) string {
 	elems := make([]string, len(val))
@@ -482,6 +492,13 @@ const (
 	mapValTmpl = `for {{if .keyValidation }}k{{ else }}_{{ end }}, {{ if .valueValidation }}v{{ else }}_{{ end }} := range {{ .target }} {
 {{- .keyValidation }}
 {{- .valueValidation }}
+}`
+
+	unionValTmpl = `switch v := {{ .target }}.(type) {
+{{- range $i, $val := .values }}
+	case {{ index $.types $i }}:
+		{{ $val }}
+{{ end -}}
 }`
 
 	userValTmpl = `if err2 := Validate{{ .name }}({{ .target }}); err2 != nil {
diff --git a/expr/hasher.go b/expr/hasher.go
index 8376b4ad..b89dd942 100644
--- a/expr/hasher.go
+++ b/expr/hasher.go
@@ -23,13 +23,12 @@ var (
 
 // Hash returns a hash value for the given data type. Two types have the same
 // hash if:
-//    - both types have the same kind
-//    - array types have elements whose types have the same hash
-//    - map types have keys and elements whose types have the same hash
-//    - user types have the same name if ignoreNames is false or ignoreFields is true
-//    - user types have the same attribute names and the attribute types have the same hash if ignoreFields is false
-//    - object attributes have the same "struct:field:xxx" tags if ignoreTags is false
-//
+//   - both types have the same kind
+//   - array types have elements whose types have the same hash
+//   - map types have keys and elements whose types have the same hash
+//   - user types have the same name if ignoreNames is false or ignoreFields is true
+//   - user types have the same attribute names and the attribute types have the same hash if ignoreFields is false
+//   - object attributes have the same "struct:field:xxx" tags if ignoreTags is false
 func Hash(dt DataType, ignoreFields, ignoreNames, ignoreTags bool) string {
 	return *hash(dt, ignoreFields, ignoreNames, ignoreTags, make(map[*Object]*string))
 }
diff --git a/grpc/codegen/service_data.go b/grpc/codegen/service_data.go
index 713edc12..156a762f 100644
--- a/grpc/codegen/service_data.go
+++ b/grpc/codegen/service_data.go
@@ -743,7 +743,8 @@ func addValidation(att *expr.AttributeExpr, attName string, sd *ServiceData, req
 			return n
 		}
 	}
-	if def := codegen.ValidationCode(att, ctx, true, expr.IsAlias(att.Type), attName); def != "" {
+	vtx := protoBufTypeContext(sd.PkgName, sd.Scope, req)
+	if def := codegen.ValidationCode(att, ut, vtx, true, expr.IsAlias(att.Type), attName); def != "" {
 		v := &ValidationData{
 			Name:    "Validate" + name,
 			Def:     def,
@@ -772,7 +773,8 @@ func collectValidations(att *expr.AttributeExpr, attName string, ctx *codegen.At
 			// Alias type - validation is generatd inline in parent type validation code.
 			return
 		}
-		def := codegen.ValidationCode(att, ctx, true, false, attName)
+		vtx := protoBufTypeContext(sd.PkgName, sd.Scope, req)
+		def := codegen.ValidationCode(att, dt, vtx, true, false, attName)
 		name := protoBufMessageName(att, sd.Scope)
 		kind := validateClient
 		if req {
@@ -809,6 +811,10 @@ func collectValidations(att *expr.AttributeExpr, attName string, ctx *codegen.At
 	case *expr.Map:
 		collectValidations(dt.KeyType, "key", ctx, req, sd)
 		collectValidations(dt.ElemType, "val", ctx, req, sd)
+	case *expr.Union:
+		for _, nat := range dt.Values {
+			collectValidations(nat.Attribute, nat.Name, ctx, req, sd)
+		}
 	}
 }
 
@@ -1290,7 +1296,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.ValidationCode(c, ctx, required, false, varn),
+			Validate:     codegen.ValidationCode(c, nil, ctx, required, false, varn),
 			DefaultValue: c.DefaultValue,
 			Example:      c.Example(expr.Root.API.Random()),
 		})
diff --git a/http/codegen/service_data.go b/http/codegen/service_data.go
index 10ef54cf..24fe8d37 100644
--- a/http/codegen/service_data.go
+++ b/http/codegen/service_data.go
@@ -660,7 +660,7 @@ func (d ServicesData) analyze(hs *expr.HTTPServiceExpr) *ServiceData {
 						var vcode string
 						if att.Validation != nil {
 							ctx := httpContext("", rd.Scope, true, false)
-							vcode = codegen.ValidationCode(att, ctx, true, expr.IsAlias(att.Type), name)
+							vcode = codegen.ValidationCode(att, nil, ctx, true, expr.IsAlias(att.Type), name)
 						}
 						initArgs[j] = &InitArgData{
 							Ref: name,
@@ -1054,7 +1054,7 @@ func buildPayloadData(e *expr.HTTPEndpointExpr, sd *ServiceData) *PayloadData {
 							Type:         pAtt.Type,
 							TypeName:     sd.Scope.GoTypeName(pAtt),
 							TypeRef:      sd.Scope.GoTypeRef(pAtt),
-							Validate:     codegen.ValidationCode(pAtt, httpsvrctx, required, expr.IsAlias(pAtt.Type), varn),
+							Validate:     codegen.ValidationCode(pAtt, nil, httpsvrctx, required, expr.IsAlias(pAtt.Type), varn),
 							DefaultValue: pAtt.DefaultValue,
 							Example:      pAtt.Example(expr.Root.API.Random()),
 						},
@@ -1154,8 +1154,8 @@ func buildPayloadData(e *expr.HTTPEndpointExpr, sd *ServiceData) *PayloadData {
 			)
 			if ut, ok := body.(expr.UserType); ok {
 				if val := ut.Attribute().Validation; val != nil {
-					svcode = codegen.ValidationCode(ut.Attribute(), httpsvrctx, true, expr.IsAlias(ut), "body")
-					cvcode = codegen.ValidationCode(ut.Attribute(), httpclictx, true, expr.IsAlias(ut), "body")
+					svcode = codegen.ValidationCode(ut.Attribute(), ut, httpsvrctx, true, expr.IsAlias(ut), "body")
+					cvcode = codegen.ValidationCode(ut.Attribute(), ut, httpclictx, true, expr.IsAlias(ut), "body")
 				}
 			}
 			serverArgs = []*InitArgData{{
@@ -1288,7 +1288,7 @@ func buildPayloadData(e *expr.HTTPEndpointExpr, sd *ServiceData) *PayloadData {
 							TypeRef:      uref,
 							Type:         uatt.Type,
 							Pointer:      sc.UsernamePointer,
-							Validate:     codegen.ValidationCode(uatt, httpsvrctx, sc.UsernameRequired, expr.IsAlias(uatt.Type), sc.UsernameAttr),
+							Validate:     codegen.ValidationCode(uatt, nil, httpsvrctx, sc.UsernameRequired, expr.IsAlias(uatt.Type), sc.UsernameAttr),
 							Example:      uatt.Example(expr.Root.API.Random()),
 						},
 					}
@@ -1310,7 +1310,7 @@ func buildPayloadData(e *expr.HTTPEndpointExpr, sd *ServiceData) *PayloadData {
 							TypeRef:      pref,
 							Type:         patt.Type,
 							Pointer:      sc.PasswordPointer,
-							Validate:     codegen.ValidationCode(patt, httpsvrctx, sc.PasswordRequired, expr.IsAlias(patt.Type), sc.PasswordAttr),
+							Validate:     codegen.ValidationCode(patt, nil, httpsvrctx, sc.PasswordRequired, expr.IsAlias(patt.Type), sc.PasswordAttr),
 							Example:      patt.Example(expr.Root.API.Random()),
 						},
 					}
@@ -1624,7 +1624,7 @@ func buildResponses(e *expr.HTTPEndpointExpr, result *expr.AttributeExpr, viewed
 							var vcode string
 							if ut, ok := resp.Body.Type.(expr.UserType); ok {
 								if val := ut.Attribute().Validation; val != nil {
-									vcode = codegen.ValidationCode(ut.Attribute(), httpclictx, true, expr.IsAlias(ut), "body")
+									vcode = codegen.ValidationCode(ut.Attribute(), ut, httpclictx, true, expr.IsAlias(ut), "body")
 								}
 							}
 							clientArgs = []*InitArgData{{
@@ -2010,7 +2010,7 @@ func buildRequestBodyType(body, att *expr.AttributeExpr, e *expr.HTTPEndpointExp
 				varname, svc.Name, e.Name())
 			if svr {
 				// generate validation code for unmarshaled type (server-side).
-				validateDef = codegen.ValidationCode(ut.Attribute(), httpctx, true, expr.IsAlias(ut), "body")
+				validateDef = codegen.ValidationCode(ut.Attribute(), ut, httpctx, true, expr.IsAlias(ut), "body")
 				if validateDef != "" {
 					validateRef = fmt.Sprintf("err = Validate%s(&body)", varname)
 				}
@@ -2025,7 +2025,7 @@ func buildRequestBodyType(body, att *expr.AttributeExpr, e *expr.HTTPEndpointExp
 			}
 			varname = sd.Scope.GoTypeRef(body)
 			ctx := codegen.NewAttributeContext(false, false, !svr, "", sd.Scope)
-			validateRef = codegen.ValidationCode(body, ctx, true, expr.IsAlias(body.Type), "body")
+			validateRef = codegen.ValidationCode(body, nil, ctx, true, expr.IsAlias(body.Type), "body")
 			desc = body.Description
 		}
 	}
@@ -2159,7 +2159,7 @@ func buildResponseBodyType(body, att *expr.AttributeExpr, loc *codegen.Location,
 				varname, svc.Name, e.Name())
 			if !svr && view == nil {
 				// generate validation code for unmarshaled type (client-side).
-				validateDef = codegen.ValidationCode(body, httpctx, true, expr.IsAlias(body.Type), "body")
+				validateDef = codegen.ValidationCode(body, ut, httpctx, true, expr.IsAlias(body.Type), "body")
 				if validateDef != "" {
 					target := "&body"
 					if expr.IsArray(ut) {
@@ -2176,12 +2176,12 @@ func buildResponseBodyType(body, att *expr.AttributeExpr, loc *codegen.Location,
 			desc = fmt.Sprintf("%s is the type of the %q service %q endpoint HTTP response body.",
 				varname, svc.Name, e.Name())
 			def = goTypeDef(sd.Scope, body, !svr, svr)
-			validateRef = codegen.ValidationCode(body, httpctx, true, expr.IsAlias(body.Type), "body")
+			validateRef = codegen.ValidationCode(body, nil, httpctx, true, expr.IsAlias(body.Type), "body")
 		} else {
 			// response body is a primitive type. They are used as non-pointers when
 			// encoding/decoding responses.
 			httpctx = httpContext("", sd.Scope, false, true)
-			validateRef = codegen.ValidationCode(body, httpctx, true, expr.IsAlias(body.Type), "body")
+			validateRef = codegen.ValidationCode(body, nil, httpctx, true, expr.IsAlias(body.Type), "body")
 			varname = sd.Scope.GoTypeRef(body)
 			desc = body.Description
 		}
@@ -2333,7 +2333,7 @@ func extractPathParams(a *expr.MappedAttributeExpr, service *expr.AttributeExpr,
 					TypeName:     scope.GoTypeName(c),
 					TypeRef:      scope.GoTypeRef(c),
 					Pointer:      false,
-					Validate:     codegen.ValidationCode(c, ctx, true, expr.IsAlias(c.Type), varn),
+					Validate:     codegen.ValidationCode(c, nil, ctx, true, expr.IsAlias(c.Type), varn),
 					DefaultValue: c.DefaultValue,
 					Example:      c.Example(expr.Root.API.Random()),
 				},
@@ -2398,7 +2398,7 @@ func extractQueryParams(a *expr.MappedAttributeExpr, service *expr.AttributeExpr
 					TypeName:     scope.GoTypeName(c),
 					TypeRef:      typeRef,
 					Pointer:      pointer,
-					Validate:     codegen.ValidationCode(c, ctx, required, expr.IsAlias(c.Type), varn),
+					Validate:     codegen.ValidationCode(c, nil, ctx, required, expr.IsAlias(c.Type), varn),
 					DefaultValue: c.DefaultValue,
 					Example:      c.Example(expr.Root.API.Random()),
 				},
@@ -2465,7 +2465,7 @@ func extractHeaders(a *expr.MappedAttributeExpr, svcAtt *expr.AttributeExpr, svc
 					Required:     required,
 					Pointer:      pointer,
 					Type:         hattr.Type,
-					Validate:     codegen.ValidationCode(hattr, svcCtx, required, expr.IsAlias(hattr.Type), varn),
+					Validate:     codegen.ValidationCode(hattr, nil, svcCtx, required, expr.IsAlias(hattr.Type), varn),
 					DefaultValue: hattr.DefaultValue,
 					Example:      hattr.Example(expr.Root.API.Random()),
 				},
@@ -2521,7 +2521,7 @@ func extractCookies(a *expr.MappedAttributeExpr, svcAtt *expr.AttributeExpr, svc
 					Required:     required,
 					Pointer:      pointer,
 					Type:         hattr.Type,
-					Validate:     codegen.ValidationCode(hattr, svcCtx, required, expr.IsAlias(hattr.Type), varn),
+					Validate:     codegen.ValidationCode(hattr, nil, svcCtx, required, expr.IsAlias(hattr.Type), varn),
 					DefaultValue: hattr.DefaultValue,
 					Example:      hattr.Example(expr.Root.API.Random()),
 				},
@@ -2611,7 +2611,7 @@ func attributeTypeData(ut expr.UserType, req, ptr, server bool, rd *ServiceData)
 		if req || !req && !server {
 			// generate validations for responses client-side and for
 			// requests server-side and CLI
-			validate = codegen.ValidationCode(ut.Attribute(), hctx, true, expr.IsAlias(ut), "body")
+			validate = codegen.ValidationCode(ut.Attribute(), ut, hctx, true, expr.IsAlias(ut), "body")
 		}
 		if validate != "" {
 			validateRef = fmt.Sprintf("err = Validate%s(v)", name)
diff --git a/http/codegen/websocket.go b/http/codegen/websocket.go
index 2e756937..f3270344 100644
--- a/http/codegen/websocket.go
+++ b/http/codegen/websocket.go
@@ -127,7 +127,7 @@ func initWebSocketData(ed *EndpointData, e *expr.HTTPEndpointExpr, sd *ServiceDa
 							if ut, ok := body.(expr.UserType); ok {
 								if val := ut.Attribute().Validation; val != nil {
 									httpctx := httpContext("", sd.Scope, true, true)
-									svcode = codegen.ValidationCode(ut.Attribute(), httpctx, true, expr.IsAlias(ut), "body")
+									svcode = codegen.ValidationCode(ut.Attribute(), ut, httpctx, true, expr.IsAlias(ut), "body")
 								}
 							}
 						}


Test patch:
diff --git a/codegen/testdata/validation_code.go b/codegen/testdata/validation_code.go
index 30bd250b..7e76edf1 100644
--- a/codegen/testdata/validation_code.go
+++ b/codegen/testdata/validation_code.go
@@ -235,29 +235,29 @@ const (
 
 	AliasTypeValidationCode = `func Validate() (err error) {
 	if target.RequiredAlias != nil {
-		err = goa.MergeErrors(err, goa.ValidatePattern("target", string(*target.RequiredAlias), "^[A-z].*[a-z]$"))
+		err = goa.MergeErrors(err, goa.ValidatePattern("target.required_alias", string(*target.RequiredAlias), "^[A-z].*[a-z]$"))
 	}
 	if target.RequiredAlias != nil {
 		if utf8.RuneCountInString(string(*target.RequiredAlias)) < 1 {
-			err = goa.MergeErrors(err, goa.InvalidLengthError("target", string(*target.RequiredAlias), utf8.RuneCountInString(string(*target.RequiredAlias)), 1, true))
+			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", string(*target.RequiredAlias), utf8.RuneCountInString(string(*target.RequiredAlias)), 10, false))
+			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", string(*target.Alias), "^[A-z].*[a-z]$"))
+		err = goa.MergeErrors(err, goa.ValidatePattern("target.alias", string(*target.Alias), "^[A-z].*[a-z]$"))
 	}
 	if target.Alias != nil {
 		if utf8.RuneCountInString(string(*target.Alias)) < 1 {
-			err = goa.MergeErrors(err, goa.InvalidLengthError("target", string(*target.Alias), utf8.RuneCountInString(string(*target.Alias)), 1, true))
+			err = goa.MergeErrors(err, goa.InvalidLengthError("target.alias", string(*target.Alias), utf8.RuneCountInString(string(*target.Alias)), 1, true))
 		}
 	}
 	if target.Alias != nil {
 		if utf8.RuneCountInString(string(*target.Alias)) > 10 {
-			err = goa.MergeErrors(err, goa.InvalidLengthError("target", string(*target.Alias), utf8.RuneCountInString(string(*target.Alias)), 10, false))
+			err = goa.MergeErrors(err, goa.InvalidLengthError("target.alias", string(*target.Alias), utf8.RuneCountInString(string(*target.Alias)), 10, false))
 		}
 	}
 }
@@ -448,6 +448,56 @@ const (
 		}
 	}
 }
+`
+	UnionValidationCode = `func Validate() (err error) {
+	if target.RequiredUnion == nil {
+		err = goa.MergeErrors(err, goa.MissingFieldError("required_union", "target"))
+	}
+	switch v := target.RequiredUnion.(type) {
+	case *Union_Int:
+		if v.Int != nil {
+			if err2 := ValidateInteger(v.Int); err2 != nil {
+				err = goa.MergeErrors(err, err2)
+			}
+		}
+
+	case *Union_Float:
+		if v.Float != nil {
+			if err2 := ValidateFloat(v.Float); err2 != nil {
+				err = goa.MergeErrors(err, err2)
+			}
+		}
+
+	case *Union_String:
+		if v.String != nil {
+			if err2 := ValidateString(v.String); err2 != nil {
+				err = goa.MergeErrors(err, err2)
+			}
+		}
+	}
+	switch v := target.Union.(type) {
+	case *Union_Int:
+		if v.Int != nil {
+			if err2 := ValidateInteger(v.Int); err2 != nil {
+				err = goa.MergeErrors(err, err2)
+			}
+		}
+
+	case *Union_Float:
+		if v.Float != nil {
+			if err2 := ValidateFloat(v.Float); err2 != nil {
+				err = goa.MergeErrors(err, err2)
+			}
+		}
+
+	case *Union_String:
+		if v.String != nil {
+			if err2 := ValidateString(v.String); err2 != nil {
+				err = goa.MergeErrors(err, err2)
+			}
+		}
+	}
+}
 `
 
 	ResultTypePointerValidationCode = `func Validate() (err error) {
@@ -471,8 +521,10 @@ const (
 `
 
 	TypeWithCollectionPointerValidationCode = `func Validate() (err error) {
-	if err2 := ValidateResultCollection(target.Collection); err2 != nil {
-		err = goa.MergeErrors(err, err2)
+	if target.Collection != nil {
+		if err2 := ValidateResultCollection(target.Collection); err2 != nil {
+			err = goa.MergeErrors(err, err2)
+		}
 	}
 }
 `
diff --git a/codegen/testdata/validation_types_dsl.go b/codegen/testdata/validation_types_dsl.go
index 0da452b9..4eddfd0d 100644
--- a/codegen/testdata/validation_types_dsl.go
+++ b/codegen/testdata/validation_types_dsl.go
@@ -116,6 +116,20 @@ var ValidationTypesDSL = func() {
 			Required("required_map")
 		})
 
+		_ = Type("Union", func() {
+			OneOf("required_union", func() {
+				Attribute("int", IntegerT)
+				Attribute("float", FloatT)
+				Attribute("string", StringT)
+			})
+			OneOf("union", func() {
+				Attribute("int", IntegerT)
+				Attribute("float", FloatT)
+				Attribute("string", StringT)
+			})
+			Required("required_union")
+		})
+
 		Result = ResultType("application/vnd.goa.result", func() {
 			TypeName("Result")
 			Attributes(func() {
diff --git a/codegen/validation_test.go b/codegen/validation_test.go
index c9db78ee..a4e095c0 100644
--- a/codegen/validation_test.go
+++ b/codegen/validation_test.go
@@ -20,6 +20,7 @@ func TestRecursiveValidationCode(t *testing.T) {
 		arrayUT  = root.UserType("ArrayUserType")
 		arrayT   = root.UserType("Array")
 		mapT     = root.UserType("Map")
+		unionT   = root.UserType("Union")
 		rtT      = root.UserType("Result")
 		rtcolT   = root.UserType("Collection")
 		colT     = root.UserType("TypeWithCollection")
@@ -52,6 +53,7 @@ func TestRecursiveValidationCode(t *testing.T) {
 		{"map-required", mapT, true, false, false, testdata.MapRequiredValidationCode},
 		{"map-pointer", mapT, false, true, false, testdata.MapPointerValidationCode},
 		{"map-use-default", mapT, false, false, true, testdata.MapUseDefaultValidationCode},
+		{"union", unionT, true, false, false, testdata.UnionValidationCode},
 		{"result-type-pointer", rtT, false, true, false, testdata.ResultTypePointerValidationCode},
 		{"collection-required", rtcolT, true, false, false, testdata.ResultCollectionPointerValidationCode},
 		{"collection-pointer", rtcolT, false, true, false, testdata.ResultCollectionPointerValidationCode},
@@ -60,7 +62,7 @@ func TestRecursiveValidationCode(t *testing.T) {
 	for _, c := range cases {
 		t.Run(c.Name, func(t *testing.T) {
 			ctx := NewAttributeContext(c.Pointer, false, c.UseDefault, "", scope)
-			code := ValidationCode(&expr.AttributeExpr{Type: c.Type}, ctx, c.Required, expr.IsAlias(c.Type), "target")
+			code := ValidationCode(&expr.AttributeExpr{Type: c.Type}, nil, ctx, c.Required, expr.IsAlias(c.Type), "target")
 			code = FormatTestCode(t, "package foo\nfunc Validate() (err error){\n"+code+"}")
 			if code != c.Code {
 				t.Errorf("invalid code, got:\n%s\ngot vs. expected:\n%s", code, Diff(t, code, c.Code))
diff --git a/http/codegen/client_body_types_test.go b/http/codegen/client_body_types_test.go
index faa825f8..12b6af8b 100644
--- a/http/codegen/client_body_types_test.go
+++ b/http/codegen/client_body_types_test.go
@@ -677,8 +677,10 @@ func ValidateMethodResultWithResultCollectionResponseBody(body *MethodResultWith
 // ValidateResulttypeResponseBody runs the validations defined on
 // ResulttypeResponseBody
 func ValidateResulttypeResponseBody(body *ResulttypeResponseBody) (err error) {
-	if err2 := ValidateRtCollectionResponseBody(body.X); err2 != nil {
-		err = goa.MergeErrors(err, err2)
+	if body.X != nil {
+		if err2 := ValidateRtCollectionResponseBody(body.X); err2 != nil {
+			err = goa.MergeErrors(err, err2)
+		}
 	}
 	return
 }