Documentation
¶
Overview ¶
Package schema is a typed-contract primitive: one Go declaration that describes a payload and turns it into machine-readable contracts.
A Typed[T] describes one typed payload — its Go struct shape, its human metadata (i18n keys derived by convention), and optionally its settings tier membership, sum-type variants, or cross-field constraints. From a single declaration the package emits:
- JSON Schema (runtime validation)
- an OpenAPI-ready component description (docs + client codegen)
- the set of i18n keys a boot-time completeness gate can check
A paired reflector (Reflect / WalkerFromType) turns the same Go type into a runtime validator directly, with no document in between — so the library validates and emits from one source of truth.
Invariants:
- Key is forever. Renaming it breaks overrides, audits, and history.
- Title is the default-locale fallback shown when i18n resolution fails.
- Derived i18n keys follow the convention documented in DerivedKeys().
- Domain metadata (data classification, etc.) rides on FieldMeta.Extensions as vendor x-* entries — the primitive stays domain-free.
Index ¶
- func ApplyDefaults(targetPtr any) error
- func ApplyDefaultsWithPresence(targetPtr any, presence map[string]json.RawMessage) error
- func Bind[T any](raw []byte, target *T, walker *Walker) error
- func WithRegistry(ctx context.Context, r *Registry) context.Context
- type AnyTyped
- type Constraint
- type ConstraintExpr
- type ConstraintKind
- type Discriminator
- type EnumOption
- type FieldError
- type FieldMeta
- type ParseFunc
- type Provider
- type ReflectValidator
- type Registry
- func (r *Registry) Lookup(t reflect.Type) *TypeSchema
- func (r *Registry) MustRegister(t reflect.Type, ts *TypeSchema) *Registry
- func (r *Registry) NewWalker(t reflect.Type) *Walker
- func (r *Registry) Register(t reflect.Type, ts *TypeSchema) error
- func (r *Registry) Require(types ...reflect.Type) error
- type Schema
- type Tier
- type TypeSchema
- type Typed
- func (t Typed[T]) AsAny() AnyTyped
- func (t Typed[T]) DerivedKeys() []string
- func (t Typed[T]) JSONSchema() map[string]any
- func (t Typed[T]) SchemaDescription() string
- func (t Typed[T]) SchemaFields() map[string]FieldMeta
- func (t Typed[T]) SchemaKey() string
- func (t Typed[T]) SchemaTiers() []Tier
- func (t Typed[T]) SchemaTitle() string
- type ValidateFunc
- type ValidationError
- type Validator
- type Variant
- type Walker
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
func ApplyDefaults ¶
ApplyDefaults walks the decoded target struct and fills any field carrying `default:"..."` whose value is still the zero value (for non-pointer fields) or nil (for pointer fields).
Both the HTTP-body path and the MCP-args path call this so the same body-default contract `default:"..."` advertises to OpenAPI applies regardless of which transport delivered the request. Without one shared call, the drift surfaces as a silent divergence: one transport applies the documented default while another sends the zero value.
Semantics (mirror the path/query binder in the HTTP layer):
- Trigger on zero value. string "" / numeric 0 / bool false / nil pointer all count as "absent" because JSON's default decode maps missing fields to the zero value. `*string` is how handlers opt out when "" is a meaningful explicit value (the pointer is nil when absent, &"" when explicitly cleared).
- Recurses into struct fields, pointer-to-struct (auto-allocating when nil), and slices-of-struct (per-element). Does NOT recurse into maps — keys are user data, not schema-defined.
- Path/query/multipart-tagged fields are skipped: their defaults are already applied by the path-and-query binder before this runs.
- Malformed default value (e.g. `default:"abc"` on an int) → silent skip. The OpenAPI spec emit already surfaces bad defaults; request-time is the wrong place to fail.
Returns nil on success. Currently only returns non-nil on unexpected structural error (should not happen with well-formed types) — callers map it to 500, not 400.
func ApplyDefaultsWithPresence ¶
func ApplyDefaultsWithPresence(targetPtr any, presence map[string]json.RawMessage) error
ApplyDefaultsWithPresence is like ApplyDefaults but skips fields the JSON explicitly carried — even if the JSON value happens to equal the Go zero value. `presence` is the top-level JSON object's keys. Without it, a request body of `{"flag": false}` against `Flag bool `default:"true"“ would be silently rewritten to true.
Presence is consulted at the top level only. Nested struct defaults fall back to zero-value semantics (matches the previous behaviour). A future enhancement could thread per-level presence maps; defer until a real handler needs it.
func Bind ¶
Bind is the consolidation point for typed-input handling: decode the raw JSON into target, apply struct-tag defaults, then (optionally) run validation. One call site for both the HTTP-body path and the MCP-args path so the same `default:"..."` / `required:"true"` / `enum:"..."` / `minimum`/`maximum` / `pattern` vocabulary advertises and behaves identically across transports.
`validator` is optional. When nil (e.g. tests, or transports that run validation elsewhere), Bind only does decode + defaults. When non-nil, validation errors are returned wrapped as a *FieldError.
Returns a *FieldError (schema's transport-agnostic field-violation type) for validation failures so callers can use errors.As to extract structured field violations. Consumers that need an HTTP / tool-error mapping adapt it at their boundary — an HTTP adapter wraps it as its own field-error type (mapping to a 400), while an MCP consumer renders it as a tool-error result. Decode / defaults errors are returned as plain wrapped errors.
func WithRegistry ¶
WithRegistry stashes an instance type registry on the context so per-request binders (path/query param parsers, JSON decode) can resolve registered types without a package global. Mount/Adapt install it once per request from the wire-injected registry; a test constructs a ctx with its own registry for full isolation.
Types ¶
type AnyTyped ¶
type AnyTyped interface {
// SchemaKey returns the Key field. Named SchemaKey (not Key) to avoid
// collision with the common "Key()" method on generic containers.
SchemaKey() string
// SchemaTitle returns the default-locale title.
SchemaTitle() string
// SchemaDescription returns the default-locale description.
SchemaDescription() string
// JSONSchema builds the JSON Schema (Draft 7 superset) for the wrapped T.
// Returns a plain map[string]any — callers emit it however they prefer
// (OpenAPI component, runtime validator input, FE codegen source).
JSONSchema() map[string]any
// DerivedKeys returns every i18n key this schema expects to resolve at
// boot. The boot-time completeness gate (P2) walks these.
DerivedKeys() []string
// SchemaTiers returns the settings-tier membership, if any.
SchemaTiers() []Tier
// SchemaFields returns the declared field metadata (not all fields of T;
// only those the author annotated).
SchemaFields() map[string]FieldMeta
}
AnyTyped is the type-erased view of Typed[T]. Containers that hold schemas of multiple T instances (a heterogeneous registry of schemas) use AnyTyped; call sites that know T use the generic form.
The interface is deliberately narrow — callers that need the underlying Go type do a type assertion to *Typed[T]. Most callers only need Key, Title, JSONSchema, or the derived i18n keys, all of which this interface exposes.
type Constraint ¶
type Constraint struct {
If ConstraintExpr
Then ConstraintExpr
Else ConstraintExpr // optional; zero value = no else branch
// RawJSONSchema is an escape hatch for constraints the DSL doesn't
// express (regex cross-reference, computed checks). Exclusive with
// If/Then/Else — if non-empty, the DSL fields are ignored.
RawJSONSchema json.RawMessage
}
Constraint is one cross-field validation rule. Compiles to a JSON Schema if/then/else fragment (when If is non-zero) or to RawJSONSchema when set.
type ConstraintExpr ¶
type ConstraintExpr struct {
Kind ConstraintKind
Field string // JSON field name
Value any // for KindEquals
Inner []ConstraintExpr
}
ConstraintExpr is a small DSL for cross-field predicates. Helpers like FieldEquals / FieldRequired produce these; authors rarely build by hand.
The DSL stays small on purpose. If a real case needs more expressive power, use Constraint.RawJSONSchema.
func AllOf ¶
func AllOf(inner ...ConstraintExpr) ConstraintExpr
AllOf produces "every inner matches". Used to bundle multiple requirements into a single Then or Else branch.
func AnyOf ¶
func AnyOf(inner ...ConstraintExpr) ConstraintExpr
AnyOf produces "at least one inner matches". Used to express disjunctions in constraint branches.
func FieldEmpty ¶
func FieldEmpty(field string) ConstraintExpr
FieldEmpty produces "field is absent OR null". Dual of FieldPresent.
func FieldEquals ¶
func FieldEquals(field string, value any) ConstraintExpr
FieldEquals produces "field == value".
func FieldPresent ¶
func FieldPresent(field string) ConstraintExpr
FieldPresent produces "field is present AND non-null". Stricter than FieldRequired, used for "you must provide an actual value here".
func FieldRequired ¶
func FieldRequired(field string) ConstraintExpr
FieldRequired produces "field must be present" (in the JSON Schema sense — key exists in the object). Allows null; use FieldPresent for non-null.
type ConstraintKind ¶
type ConstraintKind string
ConstraintKind enumerates the supported cross-field predicates.
const ( KindZero ConstraintKind = "" // no-op, sentinel for unset KindEquals ConstraintKind = "equals" // field == value KindRequired ConstraintKind = "required" // field must be present KindPresent ConstraintKind = "present" // field is set (non-nil, non-empty) KindEmpty ConstraintKind = "empty" // field is nil or empty KindAnyOf ConstraintKind = "any-of" // at least one Inner matches KindAllOf ConstraintKind = "all-of" // every Inner matches )
The supported ConstraintKind values.
type Discriminator ¶
type Discriminator struct {
PropertyName string `json:"propertyName"`
Mapping map[string]string `json:"mapping,omitempty"`
}
Discriminator drives typed narrowing of oneOf unions on the wire. When the FE client generator (@hey-api/openapi-ts) sees a schema with a Discriminator, a `switch(x.<propertyName>)` narrows `x` to the matching variant at compile time instead of the generic `unknown` fallback that raw oneOf produces.
type EnumOption ¶
EnumOption is one allowed value for an enum field. Title is the default- locale label; the i18n key is derived as "{typed.Key}.fields.{field}.enum.{value}".
type FieldError ¶
type FieldError struct {
Fields []ValidationError
}
FieldError bundles one or more ValidationErrors. It is deliberately pure: it carries the violations and nothing about HTTP status, problem-detail shape, or any application error sentinel. A consumer that wants "these violations mean 400" adds that mapping in its own HTTP adapter — the library stays transport-agnostic.
func NewFieldError ¶
func NewFieldError(location, message string) *FieldError
NewFieldError builds a FieldError with one field.
func NewFieldErrors ¶
func NewFieldErrors(fields ...ValidationError) *FieldError
NewFieldErrors builds a FieldError from a batch of violations.
func (*FieldError) Error ¶
func (e *FieldError) Error() string
Error returns a human-readable summary: "invalid: loc: msg" for a single field, "invalid: loc1: msg1; loc2: msg2" for several.
type FieldMeta ¶
type FieldMeta struct {
// Title / Description / Help mirror the outer struct's convention. All
// three feed i18n keys by derivation (see DerivedKeys).
Title string
Description string
Help string
// Format overrides the reflected format. Standard OpenAPI formats
// ("email", "uri", "date", "date-time") plus any custom format string
// a consumer recognises ("money", "percent", "uuid", ...).
Format string
// Hidden fields never appear in GET responses. Used for secrets. Settings
// storage still persists them; the API layer redacts on serialization.
Hidden bool
// Enum restricts a string field to a fixed set. Empty = no restriction.
Enum []EnumOption
// Regimes is settings-specific: field only visible when company tax
// regime matches. Empty = always visible.
Regimes []string
// Locked is settings-specific: at the org tier, users cannot override.
// Default false — the org value is a default users may personalize.
// Locking is one-way — unlocking a previously-locked setting is safe;
// locking a previously-unlocked one requires purging existing overrides.
Locked bool
// Extensions are arbitrary OpenAPI vendor-extension entries (x-* keys)
// emitted verbatim onto the field's schema. Domain concerns (e.g. data
// classification via "x-sensitivity") live here rather than as typed
// fields, keeping this metadata carrier free of any domain package.
Extensions map[string]any
// ReadOnly marks a field as server-computed. Write paths reject it;
// response serialization includes it. Example: invoice.total.
ReadOnly bool
// WriteOnce marks a field as settable on create, immutable after. Write
// paths on update reject mismatched values. Example: invoice.number.
WriteOnce bool
}
FieldMeta describes one field of the enclosing typed struct. All fields are optional — absent values inherit reflected defaults.
type ParseFunc ¶
ParseFunc parses a raw string (from a path or query parameter) into the registered type. Returns the parsed value as any and an error if parsing fails. Nil means the type cannot be used as a path/query parameter.
type Provider ¶
Provider is the minimal interface schema consumers depend on when they need to build a Walker for a Go type at wire time.
An OpenAPI emitter typically satisfies this via a wire-layer bridge: wire layers pass the bridged instance to MCP consumers so per-tool walkers are pre-built once at registration time using the same Registry the HTTP path uses (so recognitions for registered custom value types behave identically across transports).
Defining the interface here (not importing an OpenAPI type directly) lets future consumers (CLI, metrics) participate without dragging the OpenAPI-document layer into their own dep graphs.
type ReflectValidator ¶
type ReflectValidator[T any] struct { // contains filtered or unexported fields }
ReflectValidator is a reflection-based implementation of Validator[T]. Built by NewReflectValidator[T] from a schema.Provider — the inner *Walker is constructed off the provider's Registry so $refs resolve correctly.
func NewReflectValidator ¶
func NewReflectValidator[T any](src Provider) *ReflectValidator[T]
NewReflectValidator builds a typed Validator[T] from a schema.Provider — typically an OpenAPI emitter, but any source that can hand back a `*Walker` for a given Go type satisfies the interface (MCP test harnesses, future spec sources). The construction logic is: zero value of T → reflect.Type → schema lookup → typed wrapper. Built once at handler init; reuse the instance per-request — the underlying Walker pre-compiles patterns + property walkers.
func (*ReflectValidator[T]) Validate ¶
func (v *ReflectValidator[T]) Validate(req T) []ValidationError
Validate validates the request with compile-time type checking.
type Registry ¶
type Registry struct {
// contains filtered or unexported fields
}
Registry is an instance-scoped Go-type → JSON Schema mapping. Construct one per OpenAPI document (or share one) and register the types it should render specially — e.g. an ID type → {string, format:id}, a money type → $ref Money.
A Registry holds NO shared mutable state: multiple registries with different type sets coexist, so several OpenAPI specs can each register their own types and tests construct an isolated registry with no cleanup. This applies the "registries are instances, not module globals" doctrine to schema.
func NewRegistry ¶
func NewRegistry() *Registry
NewRegistry returns an empty type registry. Populate it with Register / MustRegister at wire time (never in init()).
func RegistryFromContext ¶
RegistryFromContext returns the instance type registry stashed by WithRegistry, or nil if none is present.
func (*Registry) Lookup ¶
func (r *Registry) Lookup(t reflect.Type) *TypeSchema
Lookup returns the registered TypeSchema for t, or nil if absent.
func (*Registry) MustRegister ¶
func (r *Registry) MustRegister(t reflect.Type, ts *TypeSchema) *Registry
MustRegister is Register for wire-time builders; it panics on error so a duplicate/nil surfaces at boot, and returns the registry for chaining.
func (*Registry) NewWalker ¶
NewWalker on the Registry lets *Registry satisfy the Provider interface, so a bare schema.Registry can drive Bind / NewReflectValidator with no OpenAPI layer above it.
func (*Registry) Register ¶
func (r *Registry) Register(t reflect.Type, ts *TypeSchema) error
Register maps a Go type to its schema. It fails loud on a duplicate rather than silently overwriting — a double-registration is a wiring bug.
func (*Registry) Require ¶
Require verifies every listed type is registered — the boot validator. It returns an error naming the missing types so a server refuses to start with an incomplete registry (a silently-unregistered type would otherwise emit a wrong/degraded schema months later). Call from server boot with the set of types the application relies on rendering specially.
type Schema ¶
type Schema struct {
// Reference
Ref string `json:"$ref,omitempty"`
// Type
Type string `json:"type,omitempty"`
Format string `json:"format,omitempty"`
// Metadata
Title string `json:"title,omitempty"`
Description string `json:"description,omitempty"`
Default any `json:"default,omitempty"`
Example any `json:"example,omitempty"`
Deprecated bool `json:"deprecated,omitempty"`
// Validation - numbers
Minimum *float64 `json:"minimum,omitempty"`
Maximum *float64 `json:"maximum,omitempty"`
ExclusiveMinimum *float64 `json:"exclusiveMinimum,omitempty"`
ExclusiveMaximum *float64 `json:"exclusiveMaximum,omitempty"`
MultipleOf *float64 `json:"multipleOf,omitempty"`
// Validation - strings
MinLength *int `json:"minLength,omitempty"`
MaxLength *int `json:"maxLength,omitempty"`
Pattern string `json:"pattern,omitempty"`
// Validation - arrays
Items *Schema `json:"items,omitempty"`
MinItems *int `json:"minItems,omitempty"`
MaxItems *int `json:"maxItems,omitempty"`
// Validation - objects
Properties map[string]*Schema `json:"properties,omitempty"`
Required []string `json:"required,omitempty"`
AdditionalProperties *Schema `json:"additionalProperties,omitempty"`
MinProperties *int `json:"minProperties,omitempty"`
MaxProperties *int `json:"maxProperties,omitempty"`
// Validation - enum
Enum []any `json:"enum,omitempty"`
// Composition
AllOf []*Schema `json:"allOf,omitempty"`
OneOf []*Schema `json:"oneOf,omitempty"`
AnyOf []*Schema `json:"anyOf,omitempty"`
Not *Schema `json:"not,omitempty"`
Discriminator *Discriminator `json:"discriminator,omitempty"`
// OpenAPI 3.1 specific
Nullable bool `json:"nullable,omitempty"`
ReadOnly bool `json:"readOnly,omitempty"`
WriteOnly bool `json:"writeOnly,omitempty"`
// Internal validation (not serialized to OpenAPI)
DecimalPlaces *int `json:"-"` // Max decimal places for decimal type validation
// Extensions holds OpenAPI vendor-extension fields (x-* keys) for
// this schema level. Serialized as flat siblings by MarshalJSON per
// the OpenAPI 3.1 spec. Sources: Typed/FieldMeta emits x-title-key,
// x-description-key, x-help-key, x-sensitivity, x-hidden,
// x-write-once, x-locked, x-regimes, x-enum-labels, x-enum-label-keys,
// x-tiers, etc.
Extensions map[string]any `json:"-"`
}
Schema is the JSON Schema (with OpenAPI 3.1 extensions) that drives runtime validation and document emission. The struct lives in this package (not in an OpenAPI-document layer) so consumers such as MCP, CLI, and future agentic transports can validate input without importing OpenAPI — JSON Schema is the actual standard contract; OpenAPI is one wrapper.
The OpenAPI-flavour fields ($ref, deprecated, nullable, readOnly, writeOnly, discriminator, extensions) live here too — JSON Schema itself supports nullable + ref, and the OpenAPI extensions are vendor fields that the JSON Schema spec already accommodates via `x-` keys. Keeping one struct avoids forking a parallel "schema-without-OpenAPI-bits" type for marginal purity.
func Reflect ¶
Reflect turns a Go type into a validation *Schema. Unlike an OpenAPI document reflector, it inlines nested structs (no $ref, no components) so the result is self-contained and directly walkable by NewWalker — this is what makes schema a standalone validate-from-type library, no OpenAPI registry required.
Custom types are recognised through the instance registry: a type that registered a TypeSchema resolves to that schema instead of being field-walked. Pass a nil registry for plain-stdlib reflection.
Recursion is path-guarded: a self-referential type (a struct that transitively contains itself) collapses to a shallow {"type":"object"} at the cycle edge, so reflection always terminates. Sibling fields of the same type each expand fully — the guard tracks the active build path, not every type ever seen.
func (Schema) MarshalJSON ¶
MarshalJSON serializes Schema with two transformations:
- Extension fields are flattened as sibling properties (`x-title-key: ...`), matching OpenAPI 3.1 + JSON Schema.
- `Nullable: true` is rewritten to OpenAPI 3.1's union shape. For a typed schema: `{type: ["<concrete>", "null"]}` replaces `{type: "<concrete>", nullable: true}`. For a `$ref` schema: `{anyOf: [{$ref}, {type: "null"}]}` replaces the malformed `{$ref, nullable: true}` (3.1 disallows siblings on $ref). Codegen tools (orval, openapi-generator, redocly) reject the legacy form under a 3.1 spec; this rewrite makes them happy.
Mirrors the Operation.MarshalJSON pattern so extensions at any schema level land in the spec output without struct churn.
type Tier ¶
type Tier string
Tier identifies one layer in the settings resolution chain. Ordered most-specific → most-general; first hit wins.
const ( // TierUserGlobal is keyed by (user_id) only; it follows the user across // organizations. Examples: locale, timezone, theme. TierUserGlobal Tier = "user-global" // TierUserInOrg is keyed by (user_id, org_id): a personal preference // scoped to one organization. TierUserInOrg Tier = "user-in-org" // TierCompany is keyed by (org_id, company_id): a per-entity override // within a multi-entity organization. TierCompany Tier = "company" // TierOrg is keyed by (org_id): an organization-wide default or policy. TierOrg Tier = "org" // TierDefault is hard-coded in the schema — the last-resort fallback. TierDefault Tier = "default" )
type TypeSchema ¶
type TypeSchema struct {
Type string
Format string
Description string
Pattern string
MinLength *int
MaxLength *int
Example any
Properties map[string]*Schema // For object types — defines sub-properties
Required []string // Required property names
Validate ValidateFunc // nil = skip validation (type validates itself during unmarshal)
Parse ParseFunc // nil = type cannot be parsed from path/query parameter
}
TypeSchema maps a Go type to a fixed JSON Schema representation. Register via RegisterType before any Validator or document emitter reads from the registry.
func (*TypeSchema) ToSchema ¶
func (ts *TypeSchema) ToSchema() *Schema
ToSchema converts a TypeSchema to a Schema. Used by an OpenAPI emitter when emitting the OpenAPI document and by the validator's struct-walker when descending into a registered type.
type Typed ¶
type Typed[T any] struct { // Key is the logical identifier, conventionally namespaced by owning // component: // "invoicing.settings.defaults" — component settings // "bank.widget.unmatched-feed" — widget data contract // "invoice.event.issued" — event payload // "saved-view.route.create" — HTTP operation // // Convention: {component}.{kind}.{name}. Component-first so everything // a component owns groups together under its namespace. Stable forever — // renaming a Key breaks overrides, audits, and history. Key string // Title is the default-locale human title. Required. // Used when i18n cannot resolve TitleKey (dev tools, error logs, Swagger // without a locale cookie). Not rendered in the product UI — that path // always goes through i18n key resolution. Title string // Description is the default-locale description. Optional. Description string // Fields describes the T struct's fields. Key is the JSON field name (the // value of the `json:` tag, not the Go field name). Fields with no entry // inherit reflected defaults: title = humanized JSON name, no description, // no validation beyond struct tags. Fields map[string]FieldMeta // Tiers declares which settings scopes this schema is available at, in // resolution order (first-hit wins). Only meaningful when Typed is used // as a settings schema; widget / route / event uses leave this nil. Tiers []Tier // Variants turns Typed[T] into a sum-type (tagged union). When non-empty, // T must be an interface or a struct with a discriminator field. OpenAPI // emits `oneOf` + `discriminator`. Variants []Variant // DiscriminatorField is the JSON field name carrying the variant tag. // Meaningful only when Variants is non-empty. DiscriminatorField string // Constraints express cross-field validation that struct tags cannot. // Each entry compiles to a JSON Schema if/then/else or oneOf fragment. Constraints []Constraint }
Typed is the typed contract primitive. T is a Go struct with `json:"..."` tags; reflection on T drives JSON Schema generation.
The zero-value Typed[T] is intentionally invalid (empty Key). Callers must fill Key at least; everything else has sane defaults.
func (Typed[T]) AsAny ¶
AsAny returns the type-erased view of this Typed. Used by containers that hold schemas across different Ts (e.g. a map of settings schemas, or a typed signal payload accessed via AnyTyped).
The returned value captures a pointer to the original; mutations to the original Typed after AsAny are reflected by subsequent method calls. This is rarely material — schemas are declared once at init/wire time and not mutated.
func (Typed[T]) DerivedKeys ¶
DerivedKeys returns every i18n key this schema expects to resolve at boot. A consumer's boot-time gate can cross-reference every key with its i18n bundle and halt boot on missing keys in the default locale.
Convention (see package doc):
{Key}.title
{Key}.description
{Key}.fields.{json_name}.title [per annotated field]
{Key}.fields.{json_name}.description
{Key}.fields.{json_name}.help
{Key}.fields.{json_name}.enum.{value} [per enum option]
{Key}.variants.{Tag}.title [per variant]
Fields without annotations do NOT contribute keys — the FE falls back to the humanized field name. Authors only pay the i18n cost for fields they actually want translated.
Deterministic ordering: title/description first, then fields in declaration order (map iteration is non-deterministic, so fields are sorted), then variants in slice order.
func (Typed[T]) JSONSchema ¶
JSONSchema builds a JSON Schema (Draft 7 superset) for the wrapped type. Result is a plain map[string]any; callers serialize or walk it however they like. OpenAPI component emission (P4) walks this map; runtime validators pass it through as-is.
What drives the output:
- Reflection on T for property structure + required flags
- `json:` tags for property names (`json:"-"` skips, `json:"x,omitempty"` omits from required)
- `validate:` tags for maxLength / minLength / maximum / minimum / pattern
- FieldMeta entries override reflected defaults (Format, Enum, ReadOnly, etc.)
- Variants + DiscriminatorField produce `oneOf` + `discriminator`
- Constraints produce appended `allOf` entries
Standard-library types that reflect-by-kind would describe wrongly are special-cased:
- time.Time → {type: string, format: date-time}
- json.RawMessage → {} (opaque; the widest schema)
Anything else is walked as a struct / slice / map / primitive. To give a domain type its own schema, register a TypeSchema on a Registry — the validation reflector (Reflect / WalkerFromType) consults it.
func (Typed[T]) SchemaDescription ¶
SchemaDescription implements AnyTyped.
func (Typed[T]) SchemaFields ¶
SchemaFields implements AnyTyped.
func (Typed[T]) SchemaTiers ¶
SchemaTiers implements AnyTyped.
func (Typed[T]) SchemaTitle ¶
SchemaTitle implements AnyTyped.
type ValidateFunc ¶
type ValidateFunc func(schema *Schema, val reflect.Value, path string, errors *[]ValidationError)
ValidateFunc validates a Go value of a registered type. Called during request validation instead of default type-based validation. Nil means the type is self-validating (validated during JSON unmarshal).
type ValidationError ¶
type ValidationError struct {
// Location is the JSON path where validation failed (e.g. "body.email",
// "query.page"). "body" is the request-body root; "query" is the query
// string; "path" is path parameters.
Location string `json:"location" example:"body.email"`
// Message is a human-readable description of what failed
// (e.g. "must be a valid email", "is required").
Message string `json:"message" example:"must be a valid email"`
// Value is the offending value. Omit for security-sensitive fields
// (passwords, tokens). Optional.
Value any `json:"value,omitempty"`
}
ValidationError is a single validation failure at a JSON path. It is the library's field-violation primitive — transport-agnostic: it says WHAT failed and WHERE, never how a consumer should map it to an HTTP status or error code (that's the consumer's concern).
func Field ¶
func Field(location, message string) ValidationError
Field is shorthand for a ValidationError at a location.
func (ValidationError) Error ¶
func (e ValidationError) Error() string
Error implements error so a single ValidationError can be returned directly.
type Validator ¶
type Validator[T any] interface { // Validate returns nil/empty when the value satisfies the contract. Validate(req T) []ValidationError }
Validator validates a typed request payload at the wire boundary (HTTP body, MCP tool args, …). Implemented by *ReflectValidator[T]; modules can provide custom impls when the reflection walker isn't expressive enough.
type Variant ¶
type Variant struct {
// Tag is the discriminator value. Stable forever — serialized data
// references it, renaming breaks stored rows.
Tag string
// Schema is the shape for this variant. Its own Typed[V].
Schema AnyTyped
// Title is the default-locale label. TitleKey is derived as
// "{parent.Key}.variants.{Tag}.title".
Title string
}
Variant is one alternative in a sum-type Typed. Schema is the variant's own Typed, type-erased to AnyTyped so variants can hold different Ts.
type Walker ¶
type Walker struct {
// contains filtered or unexported fields
}
Walker validates values against a schema. Pre-compiles patterns and per-property sub-walkers so per-request validation is allocation- light. Constructed via NewWalker; held by ReflectValidator[T] as the untyped engine.
func NewWalker ¶
NewWalker creates a Walker from a Schema bound to an instance-scoped type registry (nil is valid — custom-type validation then falls back to the deprecated package-global until it's removed). Pre-compiles regex patterns and recurses into properties/items, propagating types to sub-walkers.
func WalkerFromType ¶
WalkerFromType builds a validation Walker straight from a Go type, reflecting it with Reflect. This is the standalone entry point consumers use when they have a type and a registry but no OpenAPI spec.
func (*Walker) Validate ¶
func (v *Walker) Validate(value any, path string) []ValidationError
Validate validates a value against the schema. Returns a slice of validation errors (empty if valid).
func (*Walker) ValidateWithPresence ¶
func (v *Walker) ValidateWithPresence(value any, path string, presence map[string]json.RawMessage) []ValidationError
ValidateWithPresence is Validate with an optional top-level JSON presence map. When presence is non-nil, required-field checks pass for any field listed in the map even if its Go value happens to be the zero value (the JSON explicitly carried it). When presence is nil, falls back to zero-value semantics — the historical behaviour, kept for non-object bodies, the typed Validator[T] interface, and callers that haven't been ported.
Presence is consulted at the top object only. Nested objects fall back to zero-value required checks. Threading per-level presence recursively is a future enhancement; deferred until a real handler requires it.