Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion docs/validation.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ When `true`, no `Validate()` methods are generated. Use this if you don't need v

### `simple`

When `true`, all struct types use simple `validate.Struct()` validation instead of custom validation logic. This produces cleaner code but doesn't support advanced features like union type validation.
When `true`, all struct types use simple `validate.Struct()` validation instead of custom validation logic. This produces cleaner code but doesn't support advanced features like union type validation or regex `pattern` constraints.

### `response`

Expand All @@ -42,6 +42,11 @@ The following OpenAPI constraints are translated to validation tags:
| `minItems` | `min=N` | arrays |
| `maxItems` | `max=N` | arrays |
| `enum` | custom switch | string, integer enums |
| `pattern` | `runtime.ValidatePattern()` | strings |

`pattern` is a regex, which `validate.Struct()` cannot express, so it is enforced
wherever per-field/per-item validation is generated (full-mode structs, arrays, maps).
It is not enforced in `simple` mode (see below).

## Generated Code Examples

Expand Down
3 changes: 3 additions & 0 deletions examples/circular/mutual/gen.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

24 changes: 22 additions & 2 deletions examples/client/example1/example1/headers.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

71 changes: 71 additions & 0 deletions pkg/codegen/schema.go
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,11 @@ func (s GoSchema) NeedsValidation() bool {
return true
}

// If it carries a regex pattern, it needs validation
if s.needsPatternValidation() {
return true
}

// If it has union elements, it needs validation
if len(s.UnionElements) > 0 {
return true
Expand All @@ -157,6 +162,10 @@ func (s GoSchema) NeedsValidation() bool {
if len(prop.Constraints.ValidationTags) > 0 {
return true
}
// Property carries a regex pattern
if prop.needsPatternValidation() {
return true
}
// Property needs custom validation (RefType, struct, union, etc.)
if prop.needsCustomValidation() {
return true
Expand Down Expand Up @@ -216,6 +225,68 @@ func (s GoSchema) NeedsValidation() bool {
return true
}

// ValidateDecl generates the body of the Validate() method for this schema.
// It returns the Go code that should appear inside the Validate() method.
// The alias parameter is the receiver variable name (e.g., "p" for "func (p Person) Validate()").
// The validatorVar parameter is the name of the validator variable to use (e.g., "bodyTypesValidate").
func (s GoSchema) ValidateDecl(alias string, validatorVar string) string {
return s.ValidateDeclWithOptions(alias, validatorVar, false)
}

// ValidateDeclWithOptions generates the body of the Validate() method for this schema with options.
// The forceSimple parameter forces the use of simple validation (validate.Struct()) even for complex types.
func (s GoSchema) ValidateDeclWithOptions(alias string, validatorVar string, forceSimple bool) string {
// If forceSimple is true, always use simple validation for structs
if forceSimple && isStructType(s) {
return generateSimpleStructValidation(s, alias, validatorVar)
}

// OPTIMIZATION: If this is a struct with no unions anywhere in its tree,
// AND no properties need custom validation (like RefTypes),
// we can use the simple validate.Struct() approach instead of custom validation.
// This is much cleaner and more efficient.
if canUseSimpleStructValidation(s) {
return generateSimpleStructValidation(s, alias, validatorVar)
}

// Handle array types
if isArrayType(s) {
return generateArrayValidation(s, alias, validatorVar)
}

// If this schema has a RefType set, it means it's a reference to another type
// In this case, we should delegate validation to the underlying type
if isRefTypeDelegation(s) {
return generateRefTypeDelegation(s, alias)
}

// If this schema has properties but GoType is a reference to another type
// (not a struct/map/slice), delegate to the underlying type
if isTypeAliasDelegation(s) {
return generateTypeAliasDelegation(s, alias)
}

// Handle map types (from additionalProperties)
if isMapType(s) && !hasCustomValidation(s) {
return generateMapValidation(s, alias, validatorVar)
}

// For other non-struct types (slices, primitives) without custom validation
if !hasCustomValidation(s) {
return generateNonStructValidation(s, alias, validatorVar)
}

// Generate custom validation for struct properties
return generateCustomPropertyValidation(s, alias, validatorVar)
}

// needsPatternValidation reports whether this schema carries an enforceable regex pattern.
func (s GoSchema) needsPatternValidation() bool {
return s.Constraints.hasPattern() &&
isPatternValidatable(s.TypeDecl()) &&
patternCompiles(*s.Constraints.Pattern)
}

// ContainsUnions returns true if this schema or any of its nested schemas contain union types.
// This is used to determine if we can use simple validate.Struct() or need custom validation logic.
func (s GoSchema) ContainsUnions() bool {
Expand Down
5 changes: 5 additions & 0 deletions pkg/codegen/schema_constraints.go
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,11 @@ func (c Constraints) Count() int {
return count
}

// hasPattern reports whether these constraints carry a non-empty regex pattern.
func (c Constraints) hasPattern() bool {
return c.Pattern != nil && *c.Pattern != ""
}

func newConstraints(schema *base.Schema, opts ConstraintsContext) Constraints {
if schema == nil {
return Constraints{}
Expand Down
8 changes: 8 additions & 0 deletions pkg/codegen/schema_property.go
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,14 @@ func (p Property) needsCustomValidation() bool {
return true
}

// needsPatternValidation reports whether this property carries an enforceable regex pattern
// (resolved from its schema, including $ref targets).
func (p Property) needsPatternValidation() bool {
return p.Constraints.hasPattern() &&
isPatternValidatable(p.Schema.TypeDecl()) &&
patternCompiles(*p.Constraints.Pattern)
}

func createPropertyGoFieldName(jsonName string, extensions map[string]any) string {
goFieldName := jsonName
if extension, ok := extensions[extGoName]; ok {
Expand Down
Loading
Loading