Repository: getkin/kin-openapi (go)

Issue:
Add DefineNumberFormat function
Hey, 
We use a custom format with type "number" e.g.:
"eo:epsg": {
    "type": "number",
    "format": "epsg-code",
    ...
 }

There is the function DefineStringFormat implemented into openapi3 (related issues #103, #79), but it only handles elements with type "string". So there is a need for a DefineNumberFormat function.



Pull request:
openapi3: add support for number and integer format validators
This PR adds support for Number and Integer format validators.

Fixes #107

Patch:
diff --git a/.github/docs/openapi3.txt b/.github/docs/openapi3.txt
index 37791ed..e0e4c70 100644
--- a/.github/docs/openapi3.txt
+++ b/.github/docs/openapi3.txt
@@ -30,6 +30,15 @@ const (
 	// FormatOfStringForEmail pattern catches only some suspiciously wrong-looking email addresses.
 	// Use DefineStringFormat(...) if you need something stricter.
 	FormatOfStringForEmail = `^[^@]+@[^@<>",\s]+$`
+
+	// FormatOfStringByte is a regexp for base64-encoded characters, for example, "U3dhZ2dlciByb2Nrcw=="
+	FormatOfStringByte = `(^$|^[a-zA-Z0-9+/\-_]*=*$)`
+
+	// FormatOfStringDate is a RFC3339 date format regexp, for example "2017-07-21".
+	FormatOfStringDate = `^[0-9]{4}-(0[0-9]|10|11|12)-([0-2][0-9]|30|31)$`
+
+	// FormatOfStringDateTime is a RFC3339 date-time format regexp, for example "2017-07-21T17:32:28Z".
+	FormatOfStringDateTime = `^[0-9]{4}-(0[0-9]|10|11|12)-([0-2][0-9]|30|31)T[0-9]{2}:[0-9]{2}:[0-9]{2}(\.[0-9]+)?(Z|(\+|-)[0-9]{2}:[0-9]{2})?$`
 )
 const (
 	SerializationSimple         = "simple"
@@ -65,6 +74,14 @@ var (
 	// ErrSchemaInputInf may be returned when validating a number
 	ErrSchemaInputInf = errors.New("floating point Inf is not allowed")
 )
+var (
+	// SchemaStringFormats is a map of custom string format validators.
+	SchemaStringFormats = make(map[string]StringFormatValidator)
+	// SchemaNumberFormats is a map of custom number format validators.
+	SchemaNumberFormats = make(map[string]NumberFormatValidator)
+	// SchemaIntegerFormats is a map of custom integer format validators.
+	SchemaIntegerFormats = make(map[string]IntegerFormatValidator)
+)
 var DefaultReadFromURI = URIMapCache(ReadFromURIs(ReadFromHTTP(http.DefaultClient), ReadFromFile))
     DefaultReadFromURI returns a caching ReadFromURIFunc which can read remote
     HTTP URIs and local file URIs.
@@ -73,9 +90,6 @@ var ErrURINotSupported = errors.New("unsupported URI")
     ErrURINotSupported indicates the ReadFromURIFunc does not know how to handle
     a given URI.
 
-var SchemaStringFormats = make(map[string]Format, 4)
-    SchemaStringFormats allows for validating string formats
-
 
 FUNCTIONS
 
@@ -105,12 +119,27 @@ func DefineIPv4Format()
 func DefineIPv6Format()
     DefineIPv6Format opts in ipv6 format validation on top of OAS 3 spec
 
+func DefineIntegerFormatValidator(name string, validator IntegerFormatValidator)
+    DefineIntegerFormatValidator defines a custom format validator for a given
+    integer format.
+
+func DefineNumberFormatValidator(name string, validator NumberFormatValidator)
+    DefineNumberFormatValidator defines a custom format validator for a given
+    number format.
+
 func DefineStringFormat(name string, pattern string)
-    DefineStringFormat defines a new regexp pattern for a given format
+    DefineStringFormat defines a regexp pattern for a given string
+    format Deprecated: Use openapi3.DefineStringFormatValidator(name,
+    NewRegexpFormatValidator(pattern)) instead.
 
-func DefineStringFormatCallback(name string, callback FormatCallback)
-    DefineStringFormatCallback adds a validation function for a specific schema
-    format entry
+func DefineStringFormatCallback(name string, callback func(string) error)
+    DefineStringFormatCallback defines a callback function for a given
+    string format Deprecated: Use openapi3.DefineStringFormatValidator(name,
+    NewCallbackValidator(fn)) instead.
+
+func DefineStringFormatValidator(name string, validator StringFormatValidator)
+    DefineStringFormatValidator defines a custom format validator for a given
+    string format.
 
 func Float64Ptr(value float64) *float64
     Float64Ptr is a helper for defining OpenAPI schemas.
@@ -512,14 +541,22 @@ func (e *ExternalDocs) Validate(ctx context.Context, opts ...ValidationOption) e
     Validate returns an error if ExternalDocs does not comply with the OpenAPI
     spec.
 
-type Format struct {
-	// Has unexported fields.
+type FormatValidator[T any] interface {
+	Validate(value T) error
 }
-    Format represents a format validator registered by either DefineStringFormat
-    or DefineStringFormatCallback
+    FormatValidator is an interface for custom format validators.
+
+func NewCallbackValidator[T any](fn func(T) error) FormatValidator[T]
+    NewCallbackValidator creates a new FormatValidator that uses a callback
+    function to validate the value.
+
+func NewIPValidator(isIPv4 bool) FormatValidator[string]
+    NewIPValidator creates a new FormatValidator that validates the value is an
+    IP address.
 
-type FormatCallback func(value string) error
-    FormatCallback performs custom checks on exotic formats
+func NewRangeFormatValidator[T int64 | float64](min, max T) FormatValidator[T]
+    NewRangeFormatValidator creates a new FormatValidator that validates the
+    value is within a given range.
 
 type Header struct {
 	Parameter
@@ -617,6 +654,9 @@ func (info *Info) UnmarshalJSON(data []byte) error
 func (info *Info) Validate(ctx context.Context, opts ...ValidationOption) error
     Validate returns an error if Info does not comply with the OpenAPI spec.
 
+type IntegerFormatValidator = FormatValidator[int64]
+    IntegerFormatValidator is a type alias for FormatValidator[int64]
+
 type License struct {
 	Extensions map[string]any `json:"-" yaml:"-"`
 
@@ -821,6 +861,9 @@ func WithName(name string, response *Response) NewResponsesOption
 func WithStatus(status int, responseRef *ResponseRef) NewResponsesOption
     WithStatus adds a status code keyed ResponseRef
 
+type NumberFormatValidator = FormatValidator[float64]
+    NumberFormatValidator is a type alias for FormatValidator[float64]
+
 type OAuthFlow struct {
 	Extensions map[string]any `json:"-" yaml:"-"`
 
@@ -1917,6 +1960,13 @@ type SliceUniqueItemsChecker func(items []any) bool
     SliceUniqueItemsChecker is an function used to check if an given slice have
     unique items.
 
+type StringFormatValidator = FormatValidator[string]
+    StringFormatValidator is a type alias for FormatValidator[string]
+
+func NewRegexpFormatValidator(pattern string) StringFormatValidator
+    NewRegexpFormatValidator creates a new FormatValidator that uses a regular
+    expression to validate the value.
+
 type T struct {
 	Extensions map[string]any `json:"-" yaml:"-"`
 
diff --git a/README.md b/README.md
index 54e58b7..976075c 100644
--- a/README.md
+++ b/README.md
@@ -307,6 +307,7 @@ for _, path := range doc.Paths.InMatchingOrder() {
 * `openapi3.CircularReferenceError` and `openapi3.CircularReferenceCounter` are removed. `openapi3.Loader` now implements reference backtracking, so any kind of circular references should be properly resolved.
 * `InternalizeRefs` now takes a refNameResolver that has access to `openapi3.T` and more properties of the reference needing resolving.
 * The `DefaultRefNameResolver` has been updated, choosing names that will be less likely to collide with each other. Because of this internalized specs will likely change slightly.
+* `openapi3.Format` and `openapi3.FormatCallback` are removed and the type of `openapi3.SchemaStringFormats` has changed.
 
 ### v0.125.0
 * The `openapi3filter.ErrFunc` and `openapi3filter.LogFunc` func types now take the validated request's context as first argument.
diff --git a/openapi3/schema.go b/openapi3/schema.go
index 508c0d2..7be6bd3 100644
--- a/openapi3/schema.go
+++ b/openapi3/schema.go
@@ -28,12 +28,6 @@ const (
 	TypeObject  = "object"
 	TypeString  = "string"
 	TypeNull    = "null"
-
-	// constants for integer formats
-	formatMinInt32 = float64(math.MinInt32)
-	formatMaxInt32 = float64(math.MaxInt32)
-	formatMinInt64 = float64(math.MinInt64)
-	formatMaxInt64 = float64(math.MaxInt64)
 )
 
 var (
@@ -988,7 +982,7 @@ func (schema *Schema) validate(ctx context.Context, stack []*Schema) ([]*Schema,
 				switch format {
 				case "float", "double":
 				default:
-					if validationOpts.schemaFormatValidationEnabled {
+					if _, ok := SchemaNumberFormats[format]; !ok && validationOpts.schemaFormatValidationEnabled {
 						return stack, unsupportedFormat(format)
 					}
 				}
@@ -998,7 +992,7 @@ func (schema *Schema) validate(ctx context.Context, stack []*Schema) ([]*Schema,
 				switch format {
 				case "int32", "int64":
 				default:
-					if validationOpts.schemaFormatValidationEnabled {
+					if _, ok := SchemaIntegerFormats[format]; !ok && validationOpts.schemaFormatValidationEnabled {
 						return stack, unsupportedFormat(format)
 					}
 				}
@@ -1019,7 +1013,6 @@ func (schema *Schema) validate(ctx context.Context, stack []*Schema) ([]*Schema,
 				// Defined in some other specification
 				case "email", "hostname", "ipv4", "ipv6", "uri", "uri-reference":
 				default:
-					// Try to check for custom defined formats
 					if _, ok := SchemaStringFormats[format]; !ok && validationOpts.schemaFormatValidationEnabled {
 						return stack, unsupportedFormat(format)
 					}
@@ -1523,39 +1516,56 @@ func (schema *Schema) visitJSONNumber(settings *schemaValidationSettings, value
 	}
 
 	// formats
-	if requireInteger && schema.Format != "" {
-		formatMin := float64(0)
-		formatMax := float64(0)
-		switch schema.Format {
-		case "int32":
-			formatMin = formatMinInt32
-			formatMax = formatMaxInt32
-		case "int64":
-			formatMin = formatMinInt64
-			formatMax = formatMaxInt64
-		default:
-			if settings.formatValidationEnabled {
-				return unsupportedFormat(schema.Format)
-			}
-		}
-		if formatMin != 0 && formatMax != 0 && !(formatMin <= value && value <= formatMax) {
-			if settings.failfast {
-				return errSchema
-			}
-			err := &SchemaError{
-				Value:                 value,
-				Schema:                schema,
-				SchemaField:           "format",
-				Reason:                fmt.Sprintf("number must be an %s", schema.Format),
-				customizeMessageError: settings.customizeMessageError,
+	var formatStrErr string
+	var formatErr error
+	format := schema.Format
+	if format != "" {
+		if requireInteger {
+			if f, ok := SchemaIntegerFormats[format]; ok {
+				if err := f.Validate(int64(value)); err != nil {
+					var reason string
+					schemaErr := &SchemaError{}
+					if errors.As(err, &schemaErr) {
+						reason = schemaErr.Reason
+					} else {
+						reason = err.Error()
+					}
+					formatStrErr = fmt.Sprintf(`integer doesn't match the format %q (%v)`, format, reason)
+					formatErr = fmt.Errorf("integer doesn't match the format %q: %w", format, err)
+				}
 			}
-			if !settings.multiError {
-				return err
+		} else {
+			if f, ok := SchemaNumberFormats[format]; ok {
+				if err := f.Validate(value); err != nil {
+					var reason string
+					schemaErr := &SchemaError{}
+					if errors.As(err, &schemaErr) {
+						reason = schemaErr.Reason
+					} else {
+						reason = err.Error()
+					}
+					formatStrErr = fmt.Sprintf(`number doesn't match the format %q (%v)`, format, reason)
+					formatErr = fmt.Errorf("number doesn't match the format %q: %w", format, err)
+				}
 			}
-			me = append(me, err)
 		}
 	}
 
+	if formatStrErr != "" || formatErr != nil {
+		err := &SchemaError{
+			Value:                 value,
+			Schema:                schema,
+			SchemaField:           "format",
+			Reason:                formatStrErr,
+			Origin:                formatErr,
+			customizeMessageError: settings.customizeMessageError,
+		}
+		if !settings.multiError {
+			return err
+		}
+		me = append(me, err)
+	}
+
 	// "exclusiveMinimum"
 	if v := schema.ExclusiveMin; v && !(*schema.Min < value) {
 		if settings.failfast {
@@ -1749,23 +1759,16 @@ func (schema *Schema) visitJSONString(settings *schemaValidationSettings, value
 	var formatErr error
 	if format := schema.Format; format != "" {
 		if f, ok := SchemaStringFormats[format]; ok {
-			switch {
-			case f.regexp != nil && f.callback == nil:
-				if cp := f.regexp; !cp.MatchString(value) {
-					formatStrErr = fmt.Sprintf(`string doesn't match the format %q (regular expression "%s")`, format, cp.String())
-				}
-			case f.regexp == nil && f.callback != nil:
-				if err := f.callback(value); err != nil {
-					schemaErr := &SchemaError{}
-					if errors.As(err, &schemaErr) {
-						formatStrErr = fmt.Sprintf(`string doesn't match the format %q (%s)`, format, schemaErr.Reason)
-					} else {
-						formatStrErr = fmt.Sprintf(`string doesn't match the format %q (%v)`, format, err)
-					}
-					formatErr = err
+			if err := f.Validate(value); err != nil {
+				var reason string
+				schemaErr := &SchemaError{}
+				if errors.As(err, &schemaErr) {
+					reason = schemaErr.Reason
+				} else {
+					reason = err.Error()
 				}
-			default:
-				formatStrErr = fmt.Sprintf("corrupted entry %q in SchemaStringFormats", format)
+				formatStrErr = fmt.Sprintf(`string doesn't match the format %q (%v)`, format, reason)
+				formatErr = fmt.Errorf("string doesn't match the format %q: %w", format, err)
 			}
 		}
 	}
diff --git a/openapi3/schema_formats.go b/openapi3/schema_formats.go
index f69d1a2..023c266 100644
--- a/openapi3/schema_formats.go
+++ b/openapi3/schema_formats.go
@@ -2,10 +2,33 @@ package openapi3
 
 import (
 	"fmt"
+	"math"
 	"net/netip"
 	"regexp"
 )
 
+type (
+	// FormatValidator is an interface for custom format validators.
+	FormatValidator[T any] interface {
+		Validate(value T) error
+	}
+	// StringFormatValidator is a type alias for FormatValidator[string]
+	StringFormatValidator = FormatValidator[string]
+	// NumberFormatValidator is a type alias for FormatValidator[float64]
+	NumberFormatValidator = FormatValidator[float64]
+	// IntegerFormatValidator is a type alias for FormatValidator[int64]
+	IntegerFormatValidator = FormatValidator[int64]
+)
+
+var (
+	// SchemaStringFormats is a map of custom string format validators.
+	SchemaStringFormats = make(map[string]StringFormatValidator)
+	// SchemaNumberFormats is a map of custom number format validators.
+	SchemaNumberFormats = make(map[string]NumberFormatValidator)
+	// SchemaIntegerFormats is a map of custom integer format validators.
+	SchemaIntegerFormats = make(map[string]IntegerFormatValidator)
+)
+
 const (
 	// FormatOfStringForUUIDOfRFC4122 is an optional predefined format for UUID v1-v5 as specified by RFC4122
 	FormatOfStringForUUIDOfRFC4122 = `^(?:[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-5][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000)$`
@@ -13,89 +36,134 @@ const (
 	// FormatOfStringForEmail pattern catches only some suspiciously wrong-looking email addresses.
 	// Use DefineStringFormat(...) if you need something stricter.
 	FormatOfStringForEmail = `^[^@]+@[^@<>",\s]+$`
+
+	// FormatOfStringByte is a regexp for base64-encoded characters, for example, "U3dhZ2dlciByb2Nrcw=="
+	FormatOfStringByte = `(^$|^[a-zA-Z0-9+/\-_]*=*$)`
+
+	// FormatOfStringDate is a RFC3339 date format regexp, for example "2017-07-21".
+	FormatOfStringDate = `^[0-9]{4}-(0[0-9]|10|11|12)-([0-2][0-9]|30|31)$`
+
+	// FormatOfStringDateTime is a RFC3339 date-time format regexp, for example "2017-07-21T17:32:28Z".
+	FormatOfStringDateTime = `^[0-9]{4}-(0[0-9]|10|11|12)-([0-2][0-9]|30|31)T[0-9]{2}:[0-9]{2}:[0-9]{2}(\.[0-9]+)?(Z|(\+|-)[0-9]{2}:[0-9]{2})?$`
 )
 
-// FormatCallback performs custom checks on exotic formats
-type FormatCallback func(value string) error
+func init() {
+	DefineStringFormatValidator("byte", NewRegexpFormatValidator(FormatOfStringByte))
+	DefineStringFormatValidator("date", NewRegexpFormatValidator(FormatOfStringDate))
+	DefineStringFormatValidator("date-time", NewRegexpFormatValidator(FormatOfStringDateTime))
+	DefineIntegerFormatValidator("int32", NewRangeFormatValidator(int64(math.MinInt32), int64(math.MaxInt32)))
+	DefineIntegerFormatValidator("int64", NewRangeFormatValidator(int64(math.MinInt64), int64(math.MaxInt64)))
+}
+
+// DefineIPv4Format opts in ipv4 format validation on top of OAS 3 spec
+func DefineIPv4Format() {
+	DefineStringFormatValidator("ipv4", NewIPValidator(true))
+}
 
-// Format represents a format validator registered by either DefineStringFormat or DefineStringFormatCallback
-type Format struct {
-	regexp   *regexp.Regexp
-	callback FormatCallback
+// DefineIPv6Format opts in ipv6 format validation on top of OAS 3 spec
+func DefineIPv6Format() {
+	DefineStringFormatValidator("ipv6", NewIPValidator(false))
 }
 
-// SchemaStringFormats allows for validating string formats
-var SchemaStringFormats = make(map[string]Format, 4)
+type stringRegexpFormatValidator struct {
+	re *regexp.Regexp
+}
 
-// DefineStringFormat defines a new regexp pattern for a given format
-func DefineStringFormat(name string, pattern string) {
-	re, err := regexp.Compile(pattern)
-	if err != nil {
-		err := fmt.Errorf("format %q has invalid pattern %q: %w", name, pattern, err)
-		panic(err)
+func (s stringRegexpFormatValidator) Validate(value string) error {
+	if !s.re.MatchString(value) {
+		return fmt.Errorf(`string doesn't match pattern "%s"`, s.re.String())
 	}
-	SchemaStringFormats[name] = Format{regexp: re}
+	return nil
 }
 
-// DefineStringFormatCallback adds a validation function for a specific schema format entry
-func DefineStringFormatCallback(name string, callback FormatCallback) {
-	SchemaStringFormats[name] = Format{callback: callback}
+type callbackValidator[T any] struct {
+	fn func(T) error
 }
 
-func validateIPv4(ip string) error {
-	addr, err := netip.ParseAddr(ip)
-	if err != nil {
-		return &SchemaError{
-			Value:  ip,
-			Reason: "Not an IP address",
-		}
-	}
-	if !addr.Is4() {
-		return &SchemaError{
-			Value:  ip,
-			Reason: "Not an IPv4 address (it's IPv6)",
-		}
+func (c callbackValidator[T]) Validate(value T) error {
+	return c.fn(value)
+}
+
+type rangeFormat[T int64 | float64] struct {
+	min, max T
+}
+
+func (r rangeFormat[T]) Validate(value T) error {
+	if value < r.min || value > r.max {
+		return fmt.Errorf("value should be between %v and %v", r.min, r.max)
 	}
 	return nil
 }
 
-func validateIPv6(ip string) error {
-	addr, err := netip.ParseAddr(ip)
+// NewRangeFormatValidator creates a new FormatValidator that validates the value is within a given range.
+func NewRangeFormatValidator[T int64 | float64](min, max T) FormatValidator[T] {
+	return rangeFormat[T]{min: min, max: max}
+}
+
+// NewRegexpFormatValidator creates a new FormatValidator that uses a regular expression to validate the value.
+func NewRegexpFormatValidator(pattern string) StringFormatValidator {
+	re, err := regexp.Compile(pattern)
 	if err != nil {
-		return &SchemaError{
-			Value:  ip,
-			Reason: "Not an IP address",
-			Origin: err,
-		}
-	}
-	if !addr.Is6() {
-		return &SchemaError{
-			Value:  ip,
-			Reason: "Not an IPv6 address (it's IPv4)",
-		}
+		err := fmt.Errorf("string regexp format has invalid pattern %q: %w", pattern, err)
+		panic(err)
 	}
-	return nil
+	return stringRegexpFormatValidator{re: re}
 }
 
-func init() {
-	// Base64
-	// The pattern supports base64 and b./ase64url. Padding ('=') is supported.
-	DefineStringFormat("byte", `(^$|^[a-zA-Z0-9+/\-_]*=*$)`)
+// NewCallbackValidator creates a new FormatValidator that uses a callback function to validate the value.
+func NewCallbackValidator[T any](fn func(T) error) FormatValidator[T] {
+	return callbackValidator[T]{fn: fn}
+}
 
-	// date
-	DefineStringFormat("date", `^[0-9]{4}-(0[0-9]|10|11|12)-([0-2][0-9]|30|31)$`)
+// DefineStringFormatValidator defines a custom format validator for a given string format.
+func DefineStringFormatValidator(name string, validator StringFormatValidator) {
+	SchemaStringFormats[name] = validator
+}
 
-	// date-time
-	DefineStringFormat("date-time", `^[0-9]{4}-(0[0-9]|10|11|12)-([0-2][0-9]|30|31)T[0-9]{2}:[0-9]{2}:[0-9]{2}(\.[0-9]+)?(Z|(\+|-)[0-9]{2}:[0-9]{2})?$`)
+// DefineNumberFormatValidator defines a custom format validator for a given number format.
+func DefineNumberFormatValidator(name string, validator NumberFormatValidator) {
+	SchemaNumberFormats[name] = validator
+}
 
+// DefineIntegerFormatValidator defines a custom format validator for a given integer format.
+func DefineIntegerFormatValidator(name string, validator IntegerFormatValidator) {
+	SchemaIntegerFormats[name] = validator
 }
 
-// DefineIPv4Format opts in ipv4 format validation on top of OAS 3 spec
-func DefineIPv4Format() {
-	DefineStringFormatCallback("ipv4", validateIPv4)
+// DefineStringFormat defines a regexp pattern for a given string format
+// Deprecated: Use openapi3.DefineStringFormatValidator(name, NewRegexpFormatValidator(pattern)) instead.
+func DefineStringFormat(name string, pattern string) {
+	DefineStringFormatValidator(name, NewRegexpFormatValidator(pattern))
 }
 
-// DefineIPv6Format opts in ipv6 format validation on top of OAS 3 spec
-func DefineIPv6Format() {
-	DefineStringFormatCallback("ipv6", validateIPv6)
+// DefineStringFormatCallback defines a callback function for a given string format
+// Deprecated: Use openapi3.DefineStringFormatValidator(name, NewCallbackValidator(fn)) instead.
+func DefineStringFormatCallback(name string, callback func(string) error) {
+	DefineStringFormatValidator(name, NewCallbackValidator(callback))
+}
+
+// NewIPValidator creates a new FormatValidator that validates the value is an IP address.
+func NewIPValidator(isIPv4 bool) FormatValidator[string] {
+	return callbackValidator[string]{fn: func(ip string) error {
+		addr, err := netip.ParseAddr(ip)
+		if err != nil {
+			return &SchemaError{
+				Value:  ip,
+				Reason: "Not an IP address",
+			}
+		}
+		if isIPv4 && !addr.Is4() {
+			return &SchemaError{
+				Value:  ip,
+				Reason: "Not an IPv4 address (it's IPv6)",
+			}
+		}
+		if !isIPv4 && !addr.Is6() {
+			return &SchemaError{
+				Value:  ip,
+				Reason: "Not an IPv6 address (it's IPv4)",
+			}
+		}
+		return nil
+	}}
 }


Test patch:
diff --git a/openapi3/issue735_test.go b/openapi3/issue735_test.go
index ac316af..a846caa 100644
--- a/openapi3/issue735_test.go
+++ b/openapi3/issue735_test.go
@@ -16,8 +16,8 @@ type testCase struct {
 }
 
 func TestIssue735(t *testing.T) {
-	DefineStringFormat("uuid", FormatOfStringForUUIDOfRFC4122)
-	DefineStringFormat("email", FormatOfStringForEmail)
+	DefineStringFormatValidator("uuid", NewRegexpFormatValidator(FormatOfStringForUUIDOfRFC4122))
+	DefineStringFormatValidator("email", NewRegexpFormatValidator(FormatOfStringForEmail))
 	DefineIPv4Format()
 	DefineIPv6Format()
 
diff --git a/openapi3/issue82_test.go b/openapi3/issue82_test.go
new file mode 100644
index 0000000..3bf242b
--- /dev/null
+++ b/openapi3/issue82_test.go
@@ -0,0 +1,103 @@
+package openapi3
+
+import (
+	"encoding/json"
+	"testing"
+
+	"github.com/stretchr/testify/require"
+)
+
+func TestIssue82(t *testing.T) {
+	payload := map[string]interface{}{
+		"prop1": "val",
+		"prop3": "val",
+	}
+
+	schemas := []string{`
+{
+	"type": "object",
+	"additionalProperties": false,
+	"required": ["prop1"],
+	"properties": {
+		"prop1": {
+			"type": "string"
+		}
+	}
+}`, `{
+	"anyOf": [
+		{
+			"type": "object",
+			"additionalProperties": false,
+			"required": ["prop1"],
+			"properties": {
+				"prop1": {
+					"type": "string"
+				}
+			}
+		},
+		{
+			"type": "object",
+			"additionalProperties": false,
+			"properties": {
+				"prop2": {
+					"type": "string"
+				}
+			}
+		}
+	]
+}`, `{
+		"oneOf": [
+			{
+				"type": "object",
+				"additionalProperties": false,
+				"required": ["prop1"],
+				"properties": {
+					"prop1": {
+						"type": "string"
+					}
+				}
+			},
+			{
+				"type": "object",
+				"additionalProperties": false,
+				"properties": {
+					"prop2": {
+						"type": "string"
+					}
+				}
+			}
+		]
+}`, `{
+		"allOf": [
+			{
+				"type": "object",
+				"additionalProperties": false,
+				"required": ["prop1"],
+				"properties": {
+					"prop1": {
+						"type": "string"
+					}
+				}
+			},
+			{
+				"type": "object",
+				"additionalProperties": false,
+				"properties": {
+					"prop2": {
+						"type": "string"
+					}
+				}
+			}
+		]
+	}
+`}
+
+	for _, jsonSchema := range schemas {
+		var dataSchema Schema
+		err := json.Unmarshal([]byte(jsonSchema), &dataSchema)
+		require.NoError(t, err)
+
+		err = dataSchema.VisitJSON(payload)
+		require.Error(t, err)
+	}
+}
diff --git a/openapi3/schema_formats_test.go b/openapi3/schema_formats_test.go
index ac9ba64..09cf3bf 100644
--- a/openapi3/schema_formats_test.go
+++ b/openapi3/schema_formats_test.go
@@ -50,15 +50,18 @@ func TestIssue430(t *testing.T) {
 	DefineIPv4Format()
 	DefineIPv6Format()
 
+	ipv4Validator := NewIPValidator(true)
+	ipv6Validator := NewIPValidator(false)
+
 	for datum, isV4 := range data {
 		err = schema.VisitJSON(datum)
 		require.NoError(t, err)
 		if isV4 {
-			assert.Nil(t, validateIPv4(datum), "%q should be IPv4", datum)
-			assert.NotNil(t, validateIPv6(datum), "%q should not be IPv6", datum)
+			assert.Nil(t, ipv4Validator.Validate(datum), "%q should be IPv4", datum)
+			assert.NotNil(t, ipv6Validator.Validate(datum), "%q should not be IPv6", datum)
 		} else {
-			assert.NotNil(t, validateIPv4(datum), "%q should not be IPv4", datum)
-			assert.Nil(t, validateIPv6(datum), "%q should be IPv6", datum)
+			assert.NotNil(t, ipv4Validator.Validate(datum), "%q should not be IPv4", datum)
+			assert.Nil(t, ipv6Validator.Validate(datum), "%q should be IPv6", datum)
 		}
 	}
 }
@@ -66,9 +69,9 @@ func TestIssue430(t *testing.T) {
 func TestFormatCallback_WrapError(t *testing.T) {
 	var errSomething = errors.New("something error")
 
-	DefineStringFormatCallback("foobar", func(value string) error {
+	DefineStringFormatValidator("foobar", NewCallbackValidator(func(value string) error {
 		return errSomething
-	})
+	}))
 
 	s := &Schema{Format: "foobar"}
 	err := s.VisitJSONString("blablabla")
@@ -102,7 +105,7 @@ components:
 		`ip`: `123.0.0.11111`,
 	})
 
-	require.EqualError(t, err, `Error at "/ip": Not an IP address`)
+	require.EqualError(t, err, `Error at "/ip": string doesn't match the format "ipv4": Not an IP address`)
 
 	delete(SchemaStringFormats, "ipv4")
 	SchemaErrorDetailsDisabled = false
@@ -116,7 +119,7 @@ func TestUuidFormat(t *testing.T) {
 		wantErr bool
 	}
 
-	DefineStringFormat("uuid", FormatOfStringForUUIDOfRFC4122)
+	DefineStringFormatValidator("uuid", NewRegexpFormatValidator(FormatOfStringForUUIDOfRFC4122))
 	testCases := []testCase{
 		{
 			name:    "invalid",
@@ -155,3 +158,81 @@ func TestUuidFormat(t *testing.T) {
 		})
 	}
 }
+
+func TestNumberFormats(t *testing.T) {
+	type testCase struct {
+		name    string
+		typ     string
+		format  string
+		value   any
+		wantErr bool
+	}
+	DefineNumberFormatValidator("lessThan10", NewCallbackValidator(func(value float64) error {
+		if value >= 10 {
+			return fmt.Errorf("not less than 10")
+		}
+		return nil
+	}))
+	DefineIntegerFormatValidator("odd", NewCallbackValidator(func(value int64) error {
+		if value%2 == 0 {
+			return fmt.Errorf("not odd")
+		}
+		return nil
+	}))
+	testCases := []testCase{
+		{
+			name:    "invalid number",
+			value:   "test",
+			typ:     "number",
+			format:  "",
+			wantErr: true,
+		},
+		{
+			name:    "zero float64",
+			value:   0.0,
+			typ:     "number",
+			format:  "lessThan10",
+			wantErr: false,
+		},
+		{
+			name:    "11",
+			value:   11.0,
+			typ:     "number",
+			format:  "lessThan10",
+			wantErr: true,
+		},
+		{
+			name:    "odd 11",
+			value:   11.0,
+			typ:     "integer",
+			format:  "odd",
+			wantErr: false,
+		},
+		{
+			name:    "even 12",
+			value:   12.0,
+			typ:     "integer",
+			format:  "odd",
+			wantErr: true,
+		},
+	}
+	for _, tc := range testCases {
+		t.Run(tc.name, func(t *testing.T) {
+			schema := &Schema{
+				Type:   &Types{tc.typ},
+				Format: tc.format,
+			}
+			err := schema.VisitJSON(tc.value)
+			var schemaError = &SchemaError{}
+			if tc.wantErr {
+				require.Error(t, err)
+				require.ErrorAs(t, err, &schemaError)
+
+				require.NotZero(t, schemaError.Reason)
+				require.NotContains(t, schemaError.Reason, fmt.Sprint(tc.value))
+			} else {
+				require.Nil(t, err)
+			}
+		})
+	}
+}
diff --git a/openapi3/schema_issue492_test.go b/openapi3/schema_issue492_test.go
index fa5c74b..2fdfcbc 100644
--- a/openapi3/schema_issue492_test.go
+++ b/openapi3/schema_issue492_test.go
@@ -46,5 +46,5 @@ info:
 		"name": "kin-openapi",
 		"time": "2001-02-03T04:05:06:789Z",
 	})
-	require.ErrorContains(t, err, `Error at "/time": string doesn't match the format "date-time" (regular expression "^[0-9]{4}-(0[0-9]|10|11|12)-([0-2][0-9]|30|31)T[0-9]{2}:[0-9]{2}:[0-9]{2}(\.[0-9]+)?(Z|(\+|-)[0-9]{2}:[0-9]{2})?$")`)
+	require.ErrorContains(t, err, `Error at "/time": string doesn't match the format "date-time": string doesn't match pattern "^[0-9]{4}-(0[0-9]|10|11|12)-([0-2][0-9]|30|31)T[0-9]{2}:[0-9]{2}:[0-9]{2}(\.[0-9]+)?(Z|(\+|-)[0-9]{2}:[0-9]{2})?$"`)
 }
diff --git a/openapi3/schema_test.go b/openapi3/schema_test.go
index 12f0340..d678361 100644
--- a/openapi3/schema_test.go
+++ b/openapi3/schema_test.go
@@ -22,13 +22,13 @@ type schemaExample struct {
 }
 
 func TestSchemas(t *testing.T) {
-	DefineStringFormat("uuid", FormatOfStringForUUIDOfRFC4122)
+	DefineStringFormatValidator("uuid", NewRegexpFormatValidator(FormatOfStringForUUIDOfRFC4122))
 	for _, example := range schemaExamples {
-		t.Run(example.Title, testSchema(t, example))
+		t.Run(example.Title, testSchema(example))
 	}
 }
 
-func testSchema(t *testing.T, example schemaExample) func(*testing.T) {
+func testSchema(example schemaExample) func(*testing.T) {
 	return func(t *testing.T) {
 		schema := example.Schema
 		if serialized := example.Serialization; serialized != nil {