Repository: ogen-go/ogen
Pull request #740 (2023-01-05 08:26:34)

refactor: better allof
Fixes https://github.com/ogen-go/ogen/issues/681#issuecomment-1320828474


Patch:
diff --git a/gen/schema_gen_sum.go b/gen/schema_gen_sum.go
index ad4e69ea..91a8f176 100644
--- a/gen/schema_gen_sum.go
+++ b/gen/schema_gen_sum.go
@@ -481,102 +481,6 @@ func mergeNSchemes(ss []*jsonschema.Schema) (_ *jsonschema.Schema, err error) {
 }
 
 func mergeSchemes(s1, s2 *jsonschema.Schema) (_ *jsonschema.Schema, err error) {
-	if s1 == nil || s2 == nil {
-		return nil, errors.Errorf("schema is null or empty")
-	}
-
-	containsValidators := func(s *jsonschema.Schema) bool {
-		if s.Type != "" || s.Format != "" || s.Nullable || len(s.Enum) > 0 || s.DefaultSet {
-			return true
-		}
-		if s.Item != nil ||
-			s.AdditionalProperties != nil ||
-			len(s.PatternProperties) > 0 ||
-			len(s.Properties) > 0 {
-			return true
-		}
-		if len(s.OneOf) > 0 || len(s.AnyOf) > 0 || len(s.AllOf) > 0 {
-			return true
-		}
-		if s.Discriminator != nil || s.XML != nil {
-			return true
-		}
-		if len(s.Maximum) > 0 || len(s.Minimum) > 0 || len(s.MultipleOf) > 0 ||
-			s.ExclusiveMinimum || s.ExclusiveMaximum {
-			return true
-		}
-		if s.MaxLength != nil || s.MinLength != nil || len(s.Pattern) > 0 {
-			return true
-		}
-		if s.MaxItems != nil || s.MinItems != nil || s.UniqueItems {
-			return true
-		}
-		if s.MaxProperties != nil || s.MinProperties != nil {
-			return true
-		}
-		return false
-	}
-
-	switch a, b := containsValidators(s1), containsValidators(s2); [2]bool{a, b} {
-	case [2]bool{true, true}, [2]bool{false, false}:
-	case [2]bool{true, false}:
-		return shallowSchemaCopy(s1), nil
-	case [2]bool{false, true}:
-		return shallowSchemaCopy(s2), nil
-	}
-
-	if a, b := s1.Type, s2.Type; a != "" && b != "" && a != b {
-		return nil, errors.Errorf("schema type mismatch: %s and %s", a, b)
-	}
-	typ := s1.Type
-	if typ == "" {
-		typ = s2.Type
-	}
-
-	if s1.Format != s2.Format {
-		return nil, errors.Errorf("schema format mismatch: %s and %s", s1.Format, s2.Format)
-	}
-
-	enum, err := mergeEnums(s1, s2)
-	if err != nil {
-		return nil, errors.Wrap(err, "enum")
-	}
-
-	r := &jsonschema.Schema{
-		Type:        typ,
-		Format:      s1.Format,
-		Enum:        enum,
-		Nullable:    s1.Nullable || s2.Nullable,
-		Description: "Merged schema", // TODO(tdakkota): handle in a better way.
-	}
-
-	switch {
-	case !s1.DefaultSet && !s2.DefaultSet:
-		// Nothing to do.
-	case s1.DefaultSet && !s2.DefaultSet:
-		r.Default = s1.Default
-		r.DefaultSet = true
-	case !s1.DefaultSet && s2.DefaultSet:
-		r.Default = s2.Default
-		r.DefaultSet = true
-	case s1.DefaultSet && s2.DefaultSet:
-		if !reflect.DeepEqual(s1.Default, s2.Default) {
-			return nil, errors.Errorf("schemes have different defaults")
-		}
-
-		r.Default = s1.Default
-		r.DefaultSet = true
-	}
-
-	switch d1, d2 := s1.Discriminator, s2.Discriminator; {
-	case d1 != nil && d2 != nil:
-		return nil, &ErrNotImplemented{"merge discriminator"} // TODO(tdakkota): implement
-	case d1 != nil:
-		r.Discriminator = d1
-	case d2 != nil:
-		r.Discriminator = d2
-	}
-
 	// Helper functions for comparing validation fields.
 	var (
 		someU64 = func(n1, n2 *uint64, both func(n1, n2 uint64) uint64) *uint64 {
@@ -667,16 +571,128 @@ func mergeSchemes(s1, s2 *jsonschema.Schema) (_ *jsonschema.Schema, err error) {
 		}
 	)
 
-	// JSONSchema says:
-	//   To validate against allOf, the given data
-	//   must be valid against all of the given subschemas.
-	//
-	// Current implementation simply select the strictest constraints from both schemes.
-	//
-	// Note that this approach will not work with different 'pattern' or 'multipleOf' constraints
-	// because they cannot be merged.
-	switch s1.Type {
-	case jsonschema.String:
+	switch {
+	case s1 == nil && s2 == nil:
+		return nil, nil
+	case s1 != nil && s2 == nil:
+		return s1, nil
+	case s1 == nil && s2 != nil:
+		return s2, nil
+	}
+
+	containsValidators := func(s *jsonschema.Schema) bool {
+		if s.Type != "" || s.Format != "" || s.Nullable || len(s.Enum) > 0 || s.DefaultSet {
+			return true
+		}
+		if s.Item != nil ||
+			s.AdditionalProperties != nil ||
+			len(s.PatternProperties) > 0 ||
+			len(s.Properties) > 0 {
+			return true
+		}
+		if len(s.OneOf) > 0 || len(s.AnyOf) > 0 || len(s.AllOf) > 0 {
+			return true
+		}
+		if s.Discriminator != nil || s.XML != nil {
+			return true
+		}
+		if len(s.Maximum) > 0 || len(s.Minimum) > 0 || len(s.MultipleOf) > 0 ||
+			s.ExclusiveMinimum || s.ExclusiveMaximum {
+			return true
+		}
+		if s.MaxLength != nil || s.MinLength != nil || len(s.Pattern) > 0 {
+			return true
+		}
+		if s.MaxItems != nil || s.MinItems != nil || s.UniqueItems {
+			return true
+		}
+		if s.MaxProperties != nil || s.MinProperties != nil {
+			return true
+		}
+		return false
+	}
+
+	switch a, b := containsValidators(s1), containsValidators(s2); [2]bool{a, b} {
+	case [2]bool{true, true}, [2]bool{false, false}:
+	case [2]bool{true, false}:
+		return shallowSchemaCopy(s1), nil
+	case [2]bool{false, true}:
+		return shallowSchemaCopy(s2), nil
+	}
+
+	r := &jsonschema.Schema{
+		Format:      s1.Format,
+		Nullable:    s1.Nullable || s2.Nullable,
+		Description: "Merged schema", // TODO(tdakkota): handle in a better way.
+	}
+
+	// Type
+	{
+		typ, err := someStr(string(s1.Type), string(s2.Type), func(s1, s2 string) (string, error) {
+			if s1 == s2 {
+				return s1, nil
+			}
+			return "", errors.Errorf("schema type mismatch: %s and %s", s1, s2)
+		})
+		if err != nil {
+			return nil, err
+		}
+
+		r.Type = jsonschema.SchemaType(typ)
+	}
+
+	// Format
+	{
+		format, err := someStr(s1.Format, s2.Format, func(s1, s2 string) (string, error) {
+			if s1 == s2 {
+				return s1, nil
+			}
+			return "", errors.Errorf("schema format mismatch: %s and %s", s1, s2)
+		})
+		if err != nil {
+			return nil, err
+		}
+
+		r.Format = format
+	}
+
+	// Enum
+	r.Enum, err = mergeEnums(s1, s2)
+	if err != nil {
+		return nil, errors.Wrap(err, "enum")
+	}
+
+	// Default
+	switch {
+	case !s1.DefaultSet && !s2.DefaultSet:
+		// Nothing to do.
+	case s1.DefaultSet && !s2.DefaultSet:
+		r.Default = s1.Default
+		r.DefaultSet = true
+	case !s1.DefaultSet && s2.DefaultSet:
+		r.Default = s2.Default
+		r.DefaultSet = true
+	case s1.DefaultSet && s2.DefaultSet:
+		if !reflect.DeepEqual(s1.Default, s2.Default) {
+			return nil, errors.Errorf("schemes have different defaults")
+		}
+
+		r.Default = s1.Default
+		r.DefaultSet = true
+	}
+
+	// Discriminator
+	switch d1, d2 := s1.Discriminator, s2.Discriminator; {
+	case d1 != nil && d2 != nil:
+		return nil, &ErrNotImplemented{"merge discriminator"} // TODO(tdakkota): implement
+	case d1 != nil:
+		r.Discriminator = d1
+	case d2 != nil:
+		r.Discriminator = d2
+	}
+
+	// String validation
+	{
 		r.MaxLength = someU64(s1.MaxLength, s2.MaxLength, selectMinU64)
 		r.MinLength = someU64(s1.MinLength, s2.MinLength, selectMaxU64)
 		r.Pattern, err = someStr(s1.Pattern, s2.Pattern, func(s1, s2 string) (string, error) {
@@ -688,9 +704,10 @@ func mergeSchemes(s1, s2 *jsonschema.Schema) (_ *jsonschema.Schema, err error) {
 		if err != nil {
 			return nil, errors.Wrap(err, "pattern")
 		}
+	}
 
-		return r, nil
-	case jsonschema.Integer, jsonschema.Number:
+	// Integer, Number validation
+	{
 		r.Maximum = someNum(s1.Maximum, s2.Maximum, minNum)
 		s1.ExclusiveMaximum = s1.ExclusiveMaximum || s2.ExclusiveMaximum
 
@@ -704,12 +721,13 @@ func mergeSchemes(s1, s2 *jsonschema.Schema) (_ *jsonschema.Schema, err error) {
 		// We need to generate ir.Type for each schema in 'allOf' and then merge
 		// them into single *ir.Type with all the validation.
 		if !bytes.Equal(s1.MultipleOf, s2.MultipleOf) {
-			return nil, errors.Errorf("multipleOf is different")
+			return nil, errors.Errorf("multipleOf is different: %s and %s", s1.MultipleOf, s2.MultipleOf)
 		}
 		r.MultipleOf = s1.MultipleOf
-		return r, nil
+	}
 
-	case jsonschema.Array:
+	// Array validation
+	{
 		r.Item, err = mergeSchemes(s1.Item, s2.Item)
 		if err != nil {
 			return nil, errors.Wrap(err, "merge item schema")
@@ -718,12 +736,10 @@ func mergeSchemes(s1, s2 *jsonschema.Schema) (_ *jsonschema.Schema, err error) {
 		r.MinItems = someU64(s1.MinItems, s2.MinItems, selectMaxU64)
 		r.MaxItems = someU64(s1.MaxItems, s2.MaxItems, selectMinU64)
 		r.UniqueItems = s1.UniqueItems || s2.UniqueItems
-		return r, nil
-
-	case jsonschema.Null, jsonschema.Boolean:
-		return r, nil
+	}
 
-	case jsonschema.Object:
+	// Object validation
+	{
 		if len(s1.PatternProperties) > 0 || len(s2.PatternProperties) > 0 {
 			return nil, &ErrNotImplemented{Name: "allOf with patternProperties"}
 		}
@@ -747,12 +763,9 @@ func mergeSchemes(s1, s2 *jsonschema.Schema) (_ *jsonschema.Schema, err error) {
 		if err != nil {
 			return nil, errors.Wrap(err, "merge properties")
 		}
-
-		return r, nil
-
-	default:
-		return nil, &ErrNotImplemented{Name: "complex schema merging"}
 	}
+
+	return r, nil
 }
 
 // mergeProperties finds properties with identical names
diff --git a/jsonschema/parser.go b/jsonschema/parser.go
index 57d8f7d5..b5f0e03e 100644
--- a/jsonschema/parser.go
+++ b/jsonschema/parser.go
@@ -134,7 +134,7 @@ func (p *Parser) parse1(schema *RawSchema, ctx *jsonpointer.ResolveCtx, hook fun
 	return s, nil
 }
 
-func (p *Parser) parseSchema(schema *RawSchema, ctx *jsonpointer.ResolveCtx, hook func(*Schema) *Schema) (*Schema, error) {
+func (p *Parser) parseSchema(schema *RawSchema, ctx *jsonpointer.ResolveCtx, hook func(*Schema) *Schema) (_ *Schema, err error) {
 	if schema == nil {
 		return nil, nil
 	}
@@ -169,110 +169,108 @@ func (p *Parser) parseSchema(schema *RawSchema, ctx *jsonpointer.ResolveCtx, hoo
 		return s, nil
 	}
 
-	if d := schema.Default; p.inferTypes && schema.Type == "" && len(d) > 0 {
-		typ, err := inferJSONType(json.RawMessage(d))
-		if err != nil {
-			return nil, wrapField("default", err)
-		}
-		schema.Type = typ
-	}
+	if schema.Type == "" && p.inferTypes {
+		switch {
+		case len(schema.Default) > 0:
+			schema.Type, err = inferJSONType(json.RawMessage(schema.Default))
+			if err != nil {
+				return nil, wrapField("default", err)
+			}
 
-	typ := schema.Type
-	switch {
-	case len(schema.Enum) > 0:
-		if p.inferTypes && typ == "" {
-			inferred, err := inferJSONType(schema.Enum[0])
+		case len(schema.Enum) > 0:
+			schema.Type, err = inferJSONType(schema.Enum[0])
 			if err != nil {
 				return nil, errors.Wrap(err, "infer enum type")
 			}
-			typ = inferred
+		default:
+			// Try to infer schema type from properties.
+			switch {
+			case len(schema.Properties) > 0 ||
+				schema.AdditionalProperties != nil ||
+				schema.PatternProperties != nil ||
+				schema.MaxProperties != nil ||
+				schema.MinProperties != nil:
+				schema.Type = "object"
+
+			case schema.Items != nil ||
+				schema.UniqueItems ||
+				schema.MaxItems != nil ||
+				schema.MinItems != nil:
+				schema.Type = "array"
+
+			case schema.Maximum != nil ||
+				schema.Minimum != nil ||
+				schema.ExclusiveMinimum ||
+				schema.ExclusiveMaximum || // FIXME(tdakkota): check for existence instead of true?
+				schema.MultipleOf != nil:
+				schema.Type = "number"
+
+			case schema.MaxLength != nil ||
+				schema.MinLength != nil ||
+				schema.Pattern != "":
+				schema.Type = "string"
+			}
 		}
-	case len(schema.OneOf) > 0:
-		s := hook(&Schema{})
+	}
+
+	typ, ok := map[string]SchemaType{
+		"object":  Object,
+		"array":   Array,
+		"number":  Number,
+		"integer": Integer,
+		"boolean": Boolean,
+		"string":  String,
+		"null":    Null,
+		"":        Empty,
+	}[schema.Type]
+	if !ok {
+		err := errors.Errorf("unexpected schema type: %q", schema.Type)
+		return nil, wrapField("type", err)
+	}
+
+	s := hook(&Schema{
+		Type:     typ,
+		Format:   schema.Format,
+		Nullable: typ == Null,
+		// Object validators
+		MaxProperties: schema.MaxProperties,
+		MinProperties: schema.MinProperties,
+		// Array validators
+		MinItems:    schema.MinItems,
+		MaxItems:    schema.MaxItems,
+		UniqueItems: schema.UniqueItems,
+		// Number validators
+		Minimum:          schema.Minimum,
+		Maximum:          schema.Maximum,
+		ExclusiveMinimum: schema.ExclusiveMinimum,
+		ExclusiveMaximum: schema.ExclusiveMaximum,
+		MultipleOf:       schema.MultipleOf,
+		// String validators
+		MaxLength: schema.MaxLength,
+		MinLength: schema.MinLength,
+		Pattern:   schema.Pattern,
+	})
 
-		schemas, err := p.parseMany(schema.OneOf, schema.Common.Locator, ctx)
+	switch {
+	case len(schema.OneOf) > 0:
+		s.OneOf, err = p.parseMany(schema.OneOf, schema.Common.Locator, ctx)
 		if err != nil {
 			return nil, wrapField("oneOf", err)
 		}
-		s.OneOf = schemas
-
-		return s, nil
 	case len(schema.AnyOf) > 0:
-		s := hook(&Schema{
-			// Object validators
-			MaxProperties: schema.MaxProperties,
-			MinProperties: schema.MinProperties,
-			// Array validators
-			MinItems:    schema.MinItems,
-			MaxItems:    schema.MaxItems,
-			UniqueItems: schema.UniqueItems,
-			// Number validators
-			Minimum:          schema.Minimum,
-			Maximum:          schema.Maximum,
-			ExclusiveMinimum: schema.ExclusiveMinimum,
-			ExclusiveMaximum: schema.ExclusiveMaximum,
-			MultipleOf:       schema.MultipleOf,
-			// String validators
-			MaxLength: schema.MaxLength,
-			MinLength: schema.MinLength,
-			Pattern:   schema.Pattern,
-		})
-
-		schemas, err := p.parseMany(schema.AnyOf, schema.Common.Locator, ctx)
+		s.AnyOf, err = p.parseMany(schema.AnyOf, schema.Common.Locator, ctx)
 		if err != nil {
 			return nil, wrapField("anyOf", err)
 		}
-		s.AnyOf = schemas
-
-		return s, nil
 	case len(schema.AllOf) > 0:
-		s := hook(&Schema{})
-
-		schemas, err := p.parseMany(schema.AllOf, schema.Common.Locator, ctx)
+		s.AllOf, err = p.parseMany(schema.AllOf, schema.Common.Locator, ctx)
 		if err != nil {
 			return nil, wrapField("allOf", err)
 		}
-		s.AllOf = schemas
-
-		return s, nil
-	}
-
-	// Try to infer schema type from properties.
-	if p.inferTypes && typ == "" {
-		switch {
-		case len(schema.Properties) > 0 ||
-			schema.AdditionalProperties != nil ||
-			schema.PatternProperties != nil ||
-			schema.MaxProperties != nil ||
-			schema.MinProperties != nil:
-			typ = "object"
-
-		case schema.Items != nil ||
-			schema.UniqueItems ||
-			schema.MaxItems != nil ||
-			schema.MinItems != nil:
-			typ = "array"
-
-		case schema.Maximum != nil ||
-			schema.Minimum != nil ||
-			schema.ExclusiveMinimum ||
-			schema.ExclusiveMaximum || // FIXME(tdakkota): check for existence instead of true?
-			schema.MultipleOf != nil:
-			typ = "number"
-
-		case schema.MaxLength != nil ||
-			schema.MinLength != nil ||
-			schema.Pattern != "":
-			typ = "string"
-		}
 	}
 
-	switch typ {
-	case "object":
-		if schema.Items != nil {
-			err := errors.New("object cannot contain 'items' field")
-			return nil, wrapField("items", err)
-		}
+	// Object properties
+	{
 		if err := validateMinMax(
 			"Properties",
 			schema.MinProperties,
@@ -281,27 +279,21 @@ func (p *Parser) parseSchema(schema *RawSchema, ctx *jsonpointer.ResolveCtx, hoo
 			return nil, err
 		}
 
-		required := func(name string) bool {
-			return slices.Contains(schema.Required, name)
-		}
-
-		s := hook(&Schema{
-			Type:          Object,
-			MaxProperties: schema.MaxProperties,
-			MinProperties: schema.MinProperties,
-		})
-
 		if ap := schema.AdditionalProperties; ap != nil {
 			var additional bool
 			if val := ap.Bool; val != nil {
 				additional = *val
 			} else {
 				additional = true
-				item, err := p.parse(&ap.Schema, ctx)
+				if schema.Items != nil {
+					err := errors.New("both additionalProperties and items fields are set")
+					return nil, wrapField("additionalProperties", err)
+				}
+
+				s.Item, err = p.parse(&ap.Schema, ctx)
 				if err != nil {
 					return nil, wrapField("additionalProperties", err)
 				}
-				s.Item = item
 			}
 			s.AdditionalProperties = &additional
 		}
@@ -344,16 +336,18 @@ func (p *Parser) parseSchema(schema *RawSchema, ctx *jsonpointer.ResolveCtx, hoo
 				description = s.Description
 			}
 
+			required := slices.Contains(schema.Required, propSpec.Name)
 			s.Properties = append(s.Properties, Property{
 				Name:        propSpec.Name,
 				Description: description,
 				Schema:      prop,
-				Required:    required(propSpec.Name),
+				Required:    required,
 			})
 		}
-		return s, nil
+	}
 
-	case "array":
+	// Array properties
+	{
 		if err := validateMinMax(
 			"Items",
 			schema.MinItems,
@@ -362,31 +356,16 @@ func (p *Parser) parseSchema(schema *RawSchema, ctx *jsonpointer.ResolveCtx, hoo
 			return nil, err
 		}
 
-		array := hook(&Schema{
-			Type:        Array,
-			MinItems:    schema.MinItems,
-			MaxItems:    schema.MaxItems,
-			UniqueItems: schema.UniqueItems,
-		})
-
-		if schema.Items == nil {
-			// Keep items nil, we will use ir.Any for it.
-			return array, nil
-		}
-		if len(schema.Properties) > 0 {
-			err := errors.New("array cannot contain properties")
-			return nil, wrapField("properties", err)
-		}
-
-		item, err := p.parse(schema.Items, ctx)
-		if err != nil {
-			return nil, wrapField("items", err)
+		if schema.Items != nil {
+			s.Item, err = p.parse(schema.Items, ctx)
+			if err != nil {
+				return nil, wrapField("items", err)
+			}
 		}
+	}
 
-		array.Item = item
-		return array, nil
-
-	case "number", "integer":
+	// Integer, Number properties
+	{
 		if mul := schema.MultipleOf; len(mul) > 0 {
 			if err := func() error {
 				rat := new(big.Rat)
@@ -402,24 +381,10 @@ func (p *Parser) parseSchema(schema *RawSchema, ctx *jsonpointer.ResolveCtx, hoo
 				return nil, wrapField("multipleOf", err)
 			}
 		}
+	}
 
-		return hook(&Schema{
-			Type:             SchemaType(schema.Type),
-			Format:           schema.Format,
-			Minimum:          schema.Minimum,
-			Maximum:          schema.Maximum,
-			ExclusiveMinimum: schema.ExclusiveMinimum,
-			ExclusiveMaximum: schema.ExclusiveMaximum,
-			MultipleOf:       schema.MultipleOf,
-		}), nil
-
-	case "boolean":
-		return hook(&Schema{
-			Type:   Boolean,
-			Format: schema.Format,
-		}), nil
-
-	case "string":
+	// String properties
+	{
 		if err := validateMinMax(
 			"Length",
 			schema.MinLength,
@@ -427,30 +392,9 @@ func (p *Parser) parseSchema(schema *RawSchema, ctx *jsonpointer.ResolveCtx, hoo
 		); err != nil {
 			return nil, err
 		}
-
-		return hook(&Schema{
-			Type:      String,
-			Format:    schema.Format,
-			MaxLength: schema.MaxLength,
-			MinLength: schema.MinLength,
-			Pattern:   schema.Pattern,
-		}), nil
-
-	case "null":
-		return hook(&Schema{
-			Type:     Null,
-			Nullable: true,
-		}), nil
-
-	case "":
-		return hook(&Schema{
-			Format: schema.Format,
-		}), nil
-
-	default:
-		err := errors.Errorf("unexpected schema type: %q", schema.Type)
-		return nil, wrapField("type", err)
 	}
+
+	return s, nil
 }
 
 func (p *Parser) parseMany(schemas []*RawSchema, loc location.Locator, ctx *jsonpointer.ResolveCtx) ([]*Schema, error) {


Test patch:
diff --git a/_testdata/positive/allOf.yml b/_testdata/positive/allOf.yml
index 4dc484ab..db08371e 100644
--- a/_testdata/positive/allOf.yml
+++ b/_testdata/positive/allOf.yml
@@ -39,6 +39,21 @@ paths:
       responses:
         '200':
           description: ok
+  "/stringsNotype":
+    post:
+      operationId: stringsNotype
+      requestBody:
+        required: true
+        content:
+          application/json:
+            schema:
+              allOf:
+              - nullable: true
+              - maxLength: 15
+              - type: string
+      responses:
+        '200':
+          description: ok
   "/simpleObjects":
     post:
       operationId: simpleObjects
diff --git a/gen_bench_test.go b/gen_bench_test.go
index d8699ab4..b7553a38 100644
--- a/gen_bench_test.go
+++ b/gen_bench_test.go
@@ -33,6 +33,7 @@ func BenchmarkGenerator(b *testing.B) {
 				IgnoreNotImplemented: []string{
 					"all",
 				},
+				InferSchemaType: true,
 			}
 			dir := b.TempDir()
 			fs := genfs.FormattedSource{
diff --git a/internal/integration/all_of_test.go b/internal/integration/all_of_test.go
index c537907e..6573290f 100644
--- a/internal/integration/all_of_test.go
+++ b/internal/integration/all_of_test.go
@@ -3,6 +3,7 @@ package integration
 import (
 	"context"
 	"net/http/httptest"
+	"strings"
 	"testing"
 
 	"github.com/stretchr/testify/require"
@@ -30,6 +31,10 @@ func (s *allofTestServer) ObjectsWithConflictingArrayProperty(ctx context.Contex
 	return nil
 }
 
+func (s *allofTestServer) StringsNotype(ctx context.Context, req api.NilString) error {
+	return nil
+}
+
 func TestAllof(t *testing.T) {
 	var client *api.Client
 	{
@@ -54,6 +59,16 @@ func TestAllof(t *testing.T) {
 		err = client.NullableStrings(ctx, api.NewNilString("127.0.0.1"))
 		require.NoError(t, err)
 	})
+	t.Run("stringsNotype", func(t *testing.T) {
+		err := client.StringsNotype(ctx, api.NilString{})
+		require.NoError(t, err)
+
+		err = client.StringsNotype(ctx, api.NewNilString("foo"))
+		require.NoError(t, err)
+
+		err = client.StringsNotype(ctx, api.NewNilString(strings.Repeat("1", 16)))
+		require.EqualError(t, err, "validate: string: len 16 greater than maximum 15")
+	})
 	t.Run("simpleInteger", func(t *testing.T) {
 		err := client.SimpleInteger(ctx, -7)
 		require.EqualError(t, err, "validate: int: value -7 less than -5")
diff --git a/internal/integration/test_allof/oas_client_gen.go b/internal/integration/test_allof/oas_client_gen.go
index ac616700..458b478c 100644
--- a/internal/integration/test_allof/oas_client_gen.go
+++ b/internal/integration/test_allof/oas_client_gen.go
@@ -656,3 +656,89 @@ func (c *Client) sendSimpleObjects(ctx context.Context, request *SimpleObjectsRe
 
 	return result, nil
 }
+
+// StringsNotype invokes stringsNotype operation.
+//
+// POST /stringsNotype
+func (c *Client) StringsNotype(ctx context.Context, request NilString) error {
+	res, err := c.sendStringsNotype(ctx, request)
+	_ = res
+	return err
+}
+
+func (c *Client) sendStringsNotype(ctx context.Context, request NilString) (res *StringsNotypeOK, err error) {
+	otelAttrs := []attribute.KeyValue{
+		otelogen.OperationID("stringsNotype"),
+	}
+	// Validate request before sending.
+	if err := func() error {
+		if err := (validate.String{
+			MinLength:    0,
+			MinLengthSet: false,
+			MaxLength:    15,
+			MaxLengthSet: true,
+			Email:        false,
+			Hostname:     false,
+			Regex:        nil,
+		}).Validate(string(request.Value)); err != nil {
+			return errors.Wrap(err, "string")
+		}
+		return nil
+	}(); err != nil {
+		return res, errors.Wrap(err, "validate")
+	}
+
+	// Run stopwatch.
+	startTime := time.Now()
+	defer func() {
+		elapsedDuration := time.Since(startTime)
+		c.duration.Record(ctx, elapsedDuration.Microseconds(), otelAttrs...)
+	}()
+
+	// Increment request counter.
+	c.requests.Add(ctx, 1, otelAttrs...)
+
+	// Start a span for this request.
+	ctx, span := c.cfg.Tracer.Start(ctx, "StringsNotype",
+		trace.WithAttributes(otelAttrs...),
+		clientSpanKind,
+	)
+	// Track stage for error reporting.
+	var stage string
+	defer func() {
+		if err != nil {
+			span.RecordError(err)
+			span.SetStatus(codes.Error, stage)
+			c.errors.Add(ctx, 1, otelAttrs...)
+		}
+		span.End()
+	}()
+
+	stage = "BuildURL"
+	u := uri.Clone(c.requestURL(ctx))
+	u.Path += "/stringsNotype"
+
+	stage = "EncodeRequest"
+	r, err := ht.NewRequest(ctx, "POST", u, nil)
+	if err != nil {
+		return res, errors.Wrap(err, "create request")
+	}
+	if err := encodeStringsNotypeRequest(request, r); err != nil {
+		return res, errors.Wrap(err, "encode request")
+	}
+
+	stage = "SendRequest"
+	resp, err := c.cfg.Client.Do(r)
+	if err != nil {
+		return res, errors.Wrap(err, "do request")
+	}
+	defer resp.Body.Close()
+
+	stage = "DecodeResponse"
+	result, err := decodeStringsNotypeResponse(resp)
+	if err != nil {
+		return res, errors.Wrap(err, "decode response")
+	}
+
+	return result, nil
+}
diff --git a/internal/integration/test_allof/oas_handlers_gen.go b/internal/integration/test_allof/oas_handlers_gen.go
index f97d11a7..a2aedd96 100644
--- a/internal/integration/test_allof/oas_handlers_gen.go
+++ b/internal/integration/test_allof/oas_handlers_gen.go
@@ -715,3 +715,101 @@ func (s *Server) handleSimpleObjectsRequest(args [0]string, w http.ResponseWrite
 		return
 	}
 }
+
+// handleStringsNotypeRequest handles stringsNotype operation.
+//
+// POST /stringsNotype
+func (s *Server) handleStringsNotypeRequest(args [0]string, w http.ResponseWriter, r *http.Request) {
+	otelAttrs := []attribute.KeyValue{
+		otelogen.OperationID("stringsNotype"),
+	}
+
+	// Start a span for this request.
+	ctx, span := s.cfg.Tracer.Start(r.Context(), "StringsNotype",
+		trace.WithAttributes(otelAttrs...),
+		serverSpanKind,
+	)
+	defer span.End()
+
+	// Run stopwatch.
+	startTime := time.Now()
+	defer func() {
+		elapsedDuration := time.Since(startTime)
+		s.duration.Record(ctx, elapsedDuration.Microseconds(), otelAttrs...)
+	}()
+
+	// Increment request counter.
+	s.requests.Add(ctx, 1, otelAttrs...)
+
+	var (
+		recordError = func(stage string, err error) {
+			span.RecordError(err)
+			span.SetStatus(codes.Error, stage)
+			s.errors.Add(ctx, 1, otelAttrs...)
+		}
+		err          error
+		opErrContext = ogenerrors.OperationContext{
+			Name: "StringsNotype",
+			ID:   "stringsNotype",
+		}
+	)
+	request, close, err := s.decodeStringsNotypeRequest(r)
+	if err != nil {
+		err = &ogenerrors.DecodeRequestError{
+			OperationContext: opErrContext,
+			Err:              err,
+		}
+		recordError("DecodeRequest", err)
+		s.cfg.ErrorHandler(ctx, w, r, err)
+		return
+	}
+	defer func() {
+		if err := close(); err != nil {
+			recordError("CloseRequest", err)
+		}
+	}()
+
+	var response *StringsNotypeOK
+	if m := s.cfg.Middleware; m != nil {
+		mreq := middleware.Request{
+			Context:       ctx,
+			OperationName: "StringsNotype",
+			OperationID:   "stringsNotype",
+			Body:          request,
+			Params:        middleware.Parameters{},
+			Raw:           r,
+		}
+
+		type (
+			Request  = NilString
+			Params   = struct{}
+			Response = *StringsNotypeOK
+		)
+		response, err = middleware.HookMiddleware[
+			Request,
+			Params,
+			Response,
+		](
+			m,
+			mreq,
+			nil,
+			func(ctx context.Context, request Request, params Params) (response Response, err error) {
+				err = s.h.StringsNotype(ctx, request)
+				return response, err
+			},
+		)
+	} else {
+		err = s.h.StringsNotype(ctx, request)
+	}
+	if err != nil {
+		recordError("Internal", err)
+		s.cfg.ErrorHandler(ctx, w, r, err)
+		return
+	}
+
+	if err := encodeStringsNotypeResponse(response, w, span); err != nil {
+		recordError("EncodeResponse", err)
+		s.cfg.ErrorHandler(ctx, w, r, err)
+		return
+	}
+}
diff --git a/internal/integration/test_allof/oas_request_decoders_gen.go b/internal/integration/test_allof/oas_request_decoders_gen.go
index 6ad29472..535db44f 100644
--- a/internal/integration/test_allof/oas_request_decoders_gen.go
+++ b/internal/integration/test_allof/oas_request_decoders_gen.go
@@ -745,3 +745,82 @@ func (s *Server) decodeSimpleObjectsRequest(r *http.Request) (
 		return req, close, validate.InvalidContentType(ct)
 	}
 }
+
+func (s *Server) decodeStringsNotypeRequest(r *http.Request) (
+	req NilString,
+	close func() error,
+	rerr error,
+) {
+	var closers []func() error
+	close = func() error {
+		var merr error
+		// Close in reverse order, to match defer behavior.
+		for i := len(closers) - 1; i >= 0; i-- {
+			c := closers[i]
+			merr = multierr.Append(merr, c())
+		}
+		return merr
+	}
+	defer func() {
+		if rerr != nil {
+			rerr = multierr.Append(rerr, close())
+		}
+	}()
+	ct, _, err := mime.ParseMediaType(r.Header.Get("Content-Type"))
+	if err != nil {
+		return req, close, errors.Wrap(err, "parse media type")
+	}
+	switch {
+	case ct == "application/json":
+		if r.ContentLength == 0 {
+			return req, close, validate.ErrBodyRequired
+		}
+
+		var request NilString
+		buf, err := io.ReadAll(r.Body)
+		if err != nil {
+			return req, close, err
+		}
+
+		if len(buf) == 0 {
+			return req, close, validate.ErrBodyRequired
+		}
+
+		d := jx.DecodeBytes(buf)
+		if err := func() error {
+			if err := request.Decode(d); err != nil {
+				return err
+			}
+			if err := d.Skip(); err != io.EOF {
+				return errors.New("unexpected trailing data")
+			}
+			return nil
+		}(); err != nil {
+			err = &ogenerrors.DecodeBodyError{
+				ContentType: ct,
+				Body:        buf,
+				Err:         err,
+			}
+			return req, close, err
+		}
+		if err := func() error {
+			if err := (validate.String{
+				MinLength:    0,
+				MinLengthSet: false,
+				MaxLength:    15,
+				MaxLengthSet: true,
+				Email:        false,
+				Hostname:     false,
+				Regex:        nil,
+			}).Validate(string(request.Value)); err != nil {
+				return errors.Wrap(err, "string")
+			}
+			return nil
+		}(); err != nil {
+			return req, close, errors.Wrap(err, "validate")
+		}
+		return request, close, nil
+	default:
+		return req, close, validate.InvalidContentType(ct)
+	}
+}
diff --git a/internal/integration/test_allof/oas_request_encoders_gen.go b/internal/integration/test_allof/oas_request_encoders_gen.go
index 4e5889de..64aec8d3 100644
--- a/internal/integration/test_allof/oas_request_encoders_gen.go
+++ b/internal/integration/test_allof/oas_request_encoders_gen.go
@@ -230,3 +230,17 @@ func encodeSimpleObjectsRequest(
 	ht.SetBody(r, bytes.NewReader(encoded), contentType)
 	return nil
 }
+
+func encodeStringsNotypeRequest(
+	req NilString,
+	r *http.Request,
+) error {
+	const contentType = "application/json"
+	e := jx.GetEncoder()
+	{
+		req.Encode(e)
+	}
+	encoded := e.Bytes()
+	ht.SetBody(r, bytes.NewReader(encoded), contentType)
+	return nil
+}
diff --git a/internal/integration/test_allof/oas_response_decoders_gen.go b/internal/integration/test_allof/oas_response_decoders_gen.go
index 92671ae1..5aec5eca 100644
--- a/internal/integration/test_allof/oas_response_decoders_gen.go
+++ b/internal/integration/test_allof/oas_response_decoders_gen.go
@@ -70,3 +70,12 @@ func decodeSimpleObjectsResponse(resp *http.Response) (res *SimpleObjectsOK, err
 	}
 	return res, validate.UnexpectedStatusCode(resp.StatusCode)
 }
+
+func decodeStringsNotypeResponse(resp *http.Response) (res *StringsNotypeOK, err error) {
+	switch resp.StatusCode {
+	case 200:
+		// Code 200.
+		return &StringsNotypeOK{}, nil
+	}
+	return res, validate.UnexpectedStatusCode(resp.StatusCode)
+}
diff --git a/internal/integration/test_allof/oas_response_encoders_gen.go b/internal/integration/test_allof/oas_response_encoders_gen.go
index c9365f87..fe0dc721 100644
--- a/internal/integration/test_allof/oas_response_encoders_gen.go
+++ b/internal/integration/test_allof/oas_response_encoders_gen.go
@@ -57,3 +57,10 @@ func encodeSimpleObjectsResponse(response *SimpleObjectsOK, w http.ResponseWrite
 
 	return nil
 }
+
+func encodeStringsNotypeResponse(response *StringsNotypeOK, w http.ResponseWriter, span trace.Span) error {
+	w.WriteHeader(200)
+	span.SetStatus(codes.Ok, http.StatusText(200))
+
+	return nil
+}
diff --git a/internal/integration/test_allof/oas_router_gen.go b/internal/integration/test_allof/oas_router_gen.go
index f7ecf4db..4f433b69 100644
--- a/internal/integration/test_allof/oas_router_gen.go
+++ b/internal/integration/test_allof/oas_router_gen.go
@@ -155,8 +155,8 @@ func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
 						return
 					}
 				}
-			case 's': // Prefix: "simple"
-				if l := len("simple"); len(elem) >= l && elem[0:l] == "simple" {
+			case 's': // Prefix: "s"
+				if l := len("s"); len(elem) >= l && elem[0:l] == "s" {
 					elem = elem[l:]
 				} else {
 					break
@@ -166,26 +166,56 @@ func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
 					break
 				}
 				switch elem[0] {
-				case 'I': // Prefix: "Integer"
-					if l := len("Integer"); len(elem) >= l && elem[0:l] == "Integer" {
+				case 'i': // Prefix: "imple"
+					if l := len("imple"); len(elem) >= l && elem[0:l] == "imple" {
 						elem = elem[l:]
 					} else {
 						break
 					}
 
 					if len(elem) == 0 {
-						// Leaf node.
-						switch r.Method {
-						case "POST":
-							s.handleSimpleIntegerRequest([0]string{}, w, r)
-						default:
-							s.notAllowed(w, r, "POST")
+						break
+					}
+					switch elem[0] {
+					case 'I': // Prefix: "Integer"
+						if l := len("Integer"); len(elem) >= l && elem[0:l] == "Integer" {
+							elem = elem[l:]
+						} else {
+							break
 						}
 
-						return
+						if len(elem) == 0 {
+							// Leaf node.
+							switch r.Method {
+							case "POST":
+								s.handleSimpleIntegerRequest([0]string{}, w, r)
+							default:
+								s.notAllowed(w, r, "POST")
+							}
+
+							return
+						}
+					case 'O': // Prefix: "Objects"
+						if l := len("Objects"); len(elem) >= l && elem[0:l] == "Objects" {
+							elem = elem[l:]
+						} else {
+							break
+						}
+
+						if len(elem) == 0 {
+							// Leaf node.
+							switch r.Method {
+							case "POST":
+								s.handleSimpleObjectsRequest([0]string{}, w, r)
+							default:
+								s.notAllowed(w, r, "POST")
+							}
+
+							return
+						}
 					}
-				case 'O': // Prefix: "Objects"
-					if l := len("Objects"); len(elem) >= l && elem[0:l] == "Objects" {
+				case 't': // Prefix: "tringsNotype"
+					if l := len("tringsNotype"); len(elem) >= l && elem[0:l] == "tringsNotype" {
 						elem = elem[l:]
 					} else {
 						break
@@ -195,7 +225,7 @@ func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
 						// Leaf node.
 						switch r.Method {
 						case "POST":
-							s.handleSimpleObjectsRequest([0]string{}, w, r)
+							s.handleStringsNotypeRequest([0]string{}, w, r)
 						default:
 							s.notAllowed(w, r, "POST")
 						}
@@ -391,8 +421,8 @@ func (s *Server) FindPath(method string, u *url.URL) (r Route, _ bool) {
 						}
 					}
 				}
-			case 's': // Prefix: "simple"
-				if l := len("simple"); len(elem) >= l && elem[0:l] == "simple" {
+			case 's': // Prefix: "s"
+				if l := len("s"); len(elem) >= l && elem[0:l] == "s" {
 					elem = elem[l:]
 				} else {
 					break
@@ -402,28 +432,60 @@ func (s *Server) FindPath(method string, u *url.URL) (r Route, _ bool) {
 					break
 				}
 				switch elem[0] {
-				case 'I': // Prefix: "Integer"
-					if l := len("Integer"); len(elem) >= l && elem[0:l] == "Integer" {
+				case 'i': // Prefix: "imple"
+					if l := len("imple"); len(elem) >= l && elem[0:l] == "imple" {
 						elem = elem[l:]
 					} else {
 						break
 					}
 
 					if len(elem) == 0 {
-						switch method {
-						case "POST":
-							// Leaf: SimpleInteger
-							r.name = "SimpleInteger"
-							r.operationID = "simpleInteger"
-							r.args = args
-							r.count = 0
-							return r, true
-						default:
-							return
+						break
+					}
+					switch elem[0] {
+					case 'I': // Prefix: "Integer"
+						if l := len("Integer"); len(elem) >= l && elem[0:l] == "Integer" {
+							elem = elem[l:]
+						} else {
+							break
+						}
+
+						if len(elem) == 0 {
+							switch method {
+							case "POST":
+								// Leaf: SimpleInteger
+								r.name = "SimpleInteger"
+								r.operationID = "simpleInteger"
+								r.args = args
+								r.count = 0
+								return r, true
+							default:
+								return
+							}
+						}
+					case 'O': // Prefix: "Objects"
+						if l := len("Objects"); len(elem) >= l && elem[0:l] == "Objects" {
+							elem = elem[l:]
+						} else {
+							break
+						}
+
+						if len(elem) == 0 {
+							switch method {
+							case "POST":
+								// Leaf: SimpleObjects
+								r.name = "SimpleObjects"
+								r.operationID = "simpleObjects"
+								r.args = args
+								r.count = 0
+								return r, true
+							default:
+								return
+							}
 						}
 					}
-				case 'O': // Prefix: "Objects"
-					if l := len("Objects"); len(elem) >= l && elem[0:l] == "Objects" {
+				case 't': // Prefix: "tringsNotype"
+					if l := len("tringsNotype"); len(elem) >= l && elem[0:l] == "tringsNotype" {
 						elem = elem[l:]
 					} else {
 						break
@@ -432,9 +494,9 @@ func (s *Server) FindPath(method string, u *url.URL) (r Route, _ bool) {
 					if len(elem) == 0 {
 						switch method {
 						case "POST":
-							// Leaf: SimpleObjects
-							r.name = "SimpleObjects"
-							r.operationID = "simpleObjects"
+							// Leaf: StringsNotype
+							r.name = "StringsNotype"
+							r.operationID = "stringsNotype"
 							r.args = args
 							r.count = 0
 							return r, true
diff --git a/internal/integration/test_allof/oas_schemas_gen.go b/internal/integration/test_allof/oas_schemas_gen.go
index 00bd9c4c..b6e20f82 100644
--- a/internal/integration/test_allof/oas_schemas_gen.go
+++ b/internal/integration/test_allof/oas_schemas_gen.go
@@ -400,3 +400,6 @@ func (s *SimpleObjectsReq) SetFoo(val OptString) {
 func (s *SimpleObjectsReq) SetBar(val OptBool) {
 	s.Bar = val
 }
+
+// StringsNotypeOK is response for StringsNotype operation.
+type StringsNotypeOK struct{}
diff --git a/internal/integration/test_allof/oas_server_gen.go b/internal/integration/test_allof/oas_server_gen.go
index e7cfc39f..bf16cd5f 100644
--- a/internal/integration/test_allof/oas_server_gen.go
+++ b/internal/integration/test_allof/oas_server_gen.go
@@ -50,6 +50,10 @@ type Handler interface {
 	//
 	// POST /simpleObjects
 	SimpleObjects(ctx context.Context, req *SimpleObjectsReq) error
+	// StringsNotype implements stringsNotype operation.
+	//
+	// POST /stringsNotype
+	StringsNotype(ctx context.Context, req NilString) error
 }
 
 // Server implements http server based on OpenAPI v3 specification and
diff --git a/internal/integration/test_allof/oas_unimplemented_gen.go b/internal/integration/test_allof/oas_unimplemented_gen.go
index 20a80244..986d5f63 100644
--- a/internal/integration/test_allof/oas_unimplemented_gen.go
+++ b/internal/integration/test_allof/oas_unimplemented_gen.go
@@ -75,3 +75,10 @@ func (UnimplementedHandler) SimpleInteger(ctx context.Context, req int) error {
 func (UnimplementedHandler) SimpleObjects(ctx context.Context, req *SimpleObjectsReq) error {
 	return ht.ErrNotImplemented
 }
+
+// StringsNotype implements stringsNotype operation.
+//
+// POST /stringsNotype
+func (UnimplementedHandler) StringsNotype(ctx context.Context, req NilString) error {
+	return ht.ErrNotImplemented
+}