Skip to content

Latest commit

 

History

102 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Schemix

Schema-driven validation & transformation engine

CUE constraints + Bloblang dynamic expressions, unified.

Go Reference Go Version Release Codecov CI License: MIT

English | 中文


%%{init: {'flowchart': {'wrappingWidth': 340, 'curve': 'basis', 'nodeSpacing': 45, 'rankSpacing': 48}}}%%
graph TD
    SRC["<b>CUE schema text</b><br/>constraints · @blob · @meta<br/>compiled once by New()"]:::schema

    SRC --> FAST["<b>fastConstraint</b><br/>scalars only"]:::fast
    SRC --> CV["<b>CUE schema value</b><br/>structs · arrays"]:::slow

    FAST --> L1["<b>Layer 1</b> · constraint check"]:::layer
    CV --> L1

    L1 -->|all fields scalar| Z["<b>Go fast path</b><br/>no cue.Value · 0 alloc"]:::fast
    L1 -->|struct · array · blob| E["<b>lazy Encode</b><br/>LookupPath · Unify"]:::slow

    Z --> L2["<b>Layer 2</b> · @blob + @meta<br/>on the raw Go map"]:::layer
    E --> L2

    L2 --> R["<b>Result</b><br/>Valid · Output · Errors"]:::result
    L1 -.-> OBS(["Metrics · OTel<br/>per layer"]):::obs
    L2 -.-> OBS

    classDef schema fill:#dbeafe,stroke:#2563eb,color:#1e3a5f
    classDef fast fill:#dcfce7,stroke:#16a34a,color:#14532d
    classDef slow fill:#ffedd5,stroke:#ea580c,color:#7c2d12
    classDef layer fill:#f8fafc,stroke:#475569,color:#1e293b
    classDef result fill:#e0e7ff,stroke:#4f46e5,color:#312e5f
    classDef obs fill:#fafafa,stroke:#a1a1aa,stroke-dasharray:3 3,color:#52525b
Loading

Green is the allocation-free path; orange is where the input must become a cue.Value.

Table of Contents

Features

Category Capabilities
Constraints Types, regex, enums, ranges, nested structs, arrays [...{schema}], nullable null | type
Dynamic Rules Bloblang expressions — return bool for validation, other types for computed values
Built-in Validators 37+ methods: email, URL, UUID, IP, Luhn, JSON, Base64, mobile, length, range...
Custom Functions Register your own functions/methods with Bloblang-compatible API (V1 & V2 styles)
Field Control Priority groups, conditional required/skip, omit empty, fail-fast per field
Execution Three FailModes — collect all / stop at first / priority-group isolation
Performance Pre-compiled descriptors; scalar-only schemas validate in 382 ns with zero allocationscue.Context.Encode is skipped entirely
Observability MetricsRecorder hooks + OpenTelemetry tracing; ready-made schemixprom / schemixotel recorders
Error Handling Structured codes, chain API (HasCode/ErrorsByCode/ErrorsByType), custom i18n formatter
Composition Schema reuse via CUE definitions + NewFromValue, runtime introspection
Integration Method & function forms for Benthos/Redpanda Connect pipelines
Thread Safety Validator immutable after construction; Registry uses RWMutex

Install

go get github.com/mredencom/schemix@latest

Requires: Go 1.25.0 or newer

Quick Start

v, err := schemix.New(`{
    pan:      =~"^[0-9]{16}$"
    amount:   int & >0
    currency: "156" | "840"

    // Built-in validators
    luhn:       bool   @blob(this.pan.luhn_valid())
    pan_check:  bool   @blob(this.pan.has_prefix("62") || this.pan.has_prefix("4"))

    // Computed fields
    card_brand: string @blob(if this.pan.has_prefix("62") { "UnionPay" } else { "Visa" })
    fee:        number @blob(if this.currency == "156" { 0 } else { (this.amount * 0.015).ceil() })
}`)

r := v.Process(map[string]any{
    "pan": "4111111111111111", "amount": int64(10000), "currency": "840",
})

r.Valid                // true
r.Output["card_brand"] // "Visa"
r.Output["fee"]        // 150

Built-in Validators

All methods are available automatically in @blob() expressions — no registration needed.

String Format

Method Usage Description
is_email() this.email.is_email() Email address format
is_url() this.link.is_url() URL with scheme
is_full_url() this.cb.is_full_url() Must start with http/https
is_uuid() this.id.is_uuid() UUID any version
is_uuid3/4/5() this.id.is_uuid4() Specific UUID version
is_ip() this.host.is_ip() IPv4 or IPv6
is_ipv4() / is_ipv6() this.ip.is_ipv4() Specific IP version
is_cidr() this.net.is_cidr() CIDR notation
is_mac() this.mac.is_mac() MAC address
is_dns_name() this.host.is_dns_name() DNS hostname
is_json() this.body.is_json() Valid JSON string
is_base64() this.token.is_base64() Base64 encoded
is_hex() this.hash.is_hex() Hexadecimal string
is_hex_color() this.color.is_hex_color() #RGB or #RRGGBB
is_rgb_color() this.color.is_rgb_color() rgb(r,g,b)
is_data_uri() this.img.is_data_uri() data:mime;base64,...
is_latitude() this.lat.is_latitude() -90 to 90
is_longitude() this.lng.is_longitude() -180 to 180
is_isbn10/13() this.isbn.is_isbn13() ISBN format
is_cn_mobile() this.phone.is_cn_mobile() China mobile (1xx)

Character Type

Method Usage Description
is_alpha() this.name.is_alpha() Letters only
is_alpha_num() this.code.is_alpha_num() Letters + digits
is_alpha_dash() this.slug.is_alpha_dash() Letters + digits + -_
is_numeric() this.pin.is_numeric() Digits only (0-9)
is_number() this.val.is_number() Number string (±, decimal)
is_ascii() this.s.is_ascii() ASCII only
is_printable_ascii() this.s.is_printable_ascii() Printable ASCII (32-126)
is_multibyte() this.s.is_multibyte() Contains multibyte chars

String Checks

Method Usage Description
not_blank() this.name.not_blank() Not empty/whitespace
has_whitespace() this.s.has_whitespace() Contains whitespace

Length & Range

Method Usage Description
len_between(min,max) this.s.len_between(min:3, max:20) String/slice/map length
min_len(n) this.s.min_len(n: 3) Minimum length
max_len(n) this.s.max_len(n: 100) Maximum length
str_len(min,max) this.s.str_len(min:2, max:10) Rune count range
between(min,max) this.age.between(min:0, max:150) Numeric range (inclusive)

Financial

Method Usage Description
luhn_valid() this.pan.luhn_valid() Luhn checksum (card numbers)

Date Functions

Function Usage Description
is_valid_date(d) is_valid_date(this.date) Parseable date string
is_past_date(d) is_past_date(this.birthday) Date is in the past
is_future_date(d) is_future_date(this.expiry) Date is in the future

Comparison Functions

Function Usage Description
in_list(value, candidates) in_list(this.status, ["active","pending"]) Returns true if value is in the list

API Validation

Pre-compile at startup, validate per request with zero compilation overhead:

var userSchema = schemix.MustNew(`{
    username: =~"^[a-zA-Z][a-zA-Z0-9_]{2,20}$"
    email:    string @blob(this.email.is_email())
    password: string @blob(this.password.len_between(min: 8, max: 64))
    age:      int    @blob(this.age.between(min: 13, max: 150))
    role:     "admin" | "user" | "guest"
}`, schemix.WithErrorFormatter(apiFormatter))

func CreateUser(w http.ResponseWriter, req *http.Request) {
    var body map[string]any
    json.NewDecoder(req.Body).Decode(&body)

    r := userSchema.ProcessWithMode(body, schemix.FailAll)
    if !r.Valid {
        status := http.StatusBadRequest
        if r.HasCode(schemix.CodeRequiredMissing) {
            status = http.StatusUnprocessableEntity
        }
        w.WriteHeader(status)
        json.NewEncoder(w).Encode(map[string]any{
            "error":   "validation_failed",
            "details": r.Errors,
        })
        return
    }
    // use r.Output ...
}

Schema Syntax

CUE Constraints

Syntax Meaning Example
string / int / float / bool Type constraint name: string
& >=N & <=M Range age: int & >=0 & <=150
=~"regex" Regex match pan: =~"^[0-9]{16}$"
"a" | "b" Enum currency: "156" | "840"
? Optional field memo?: string
null | type Nullable memo: null | string
{...} Nested struct address: { city: string }
[...{schema}] Array of schema items: [...{id: string}]

@blob() — Bloblang Expressions

Return Type Behavior Example
bool = true Validation passes @blob(this.amount > 0)
bool = false Validation fails (→ E2B01) @blob(this.age >= 18)
Non-bool Computed value → Output @blob(this.first + " " + this.last)
Comma-separated AND — each independent @blob(expr1, expr2)

@meta() — Field Behavior Control

Parameter Type Meaning
priority=N int Execution priority (lower = earlier)
optional flag No error if field missing
conditional flag Alias for optional; documents intent when paired with required_if
skip_empty flag Skip validation when empty
fail_fast flag Skip remaining rules on failure
omit_if_skip flag Remove from Output when skipped
omit_empty flag Remove from Output when empty
required_if=expr bloblang Conditionally required
skip_if=expr bloblang Conditionally skip
Combined Example
{
    payment_type: "credit" | "debit"
    cvv?: string @meta(conditional, required_if=this.payment_type == "credit")

    pan: =~"^[0-9]{16}$" @meta(priority=1)
    luhn_check: bool @blob(this.pan.luhn_valid()) @meta(priority=2)

    memo?: string @meta(optional, omit_empty)
    fee?: number @meta(optional, skip_if=this.payment_type == "debit", omit_if_skip)
}

@meta(optional) and @meta(conditional) also relax the CUE layer. A field carrying either flag is treated as absent-tolerant even when the CUE syntax declares it required, so cvv: string @meta(conditional, …) and cvv?: string @meta(conditional, …) behave identically: no E1M01 is raised, required_if gets to run, and a missing cvv on a credit payment reports E3C01. Writing the ? is preferred for clarity — it keeps the CUE layer and the @meta layer stating the same thing — but it is not required for correctness.

conditional and optional are currently interchangeable — conditional implies optional and no other behavior distinguishes them. Prefer conditional when the field's presence is governed by required_if, as documentation of intent.

Arrays

Element structure is expressed with CUE; cross-element rules and per-element computation go on the array field itself:

{
    items: [...{
        product:   =~"^.{3,50}$"
        price:     number & >0
        qty:       int & >=1
        subtotal?: number              // computed — must be optional, see below
    }] @blob(
        this.items.length() > 0,                                        // rule
        this.items.map_each(this.price * this.qty).sum() <= 100000,      // rule
        this.items.map_each(this.merge({"subtotal": this.price * this.qty}))  // transform
    )
}

Comma-separated expressions are independent: those returning bool validate, and a non-bool return replaces the array, which is how each element gets a computed field.

Error paths differ by layer — prefer CUE for anything it can express, because only CUE reports the element index:

Violated Code Path
CUE element constraint E1R01 / E1T01 / E1F01 items[1].price
@blob rule E2B01 items

Useful Bloblang array methods: all(i -> …), any(i -> …), length(), map_each(…), filter(i -> …), index(n), sum().

all() returns false for an empty array — unlike JavaScript every() or Python all(). Spell out the intent: this.items.length() > 0 && this.items.all(…) to require non-empty, or this.items.length() == 0 || this.items.all(…) to allow empty.

Computed element fields must be declared optional (subtotal?: number). CUE runs before @blob, so a required field that the rule is supposed to produce fails with E1M01 before the rule ever executes.

Attributes inside an element schema are rejected. items: [...{qty: int @blob(…)}] makes New() return an error, because rules are compiled per field path and an element index is unknown until runtime — the attribute would be silently dropped and invalid data would pass. The error names the offending path and the supported rewrite.

Custom Functions & Methods

Register custom validation logic using the same API as Bloblang — isolated per Validator:

// Function style: my_func(args...)
v, _ := schemix.New(schema, schemix.WithFunction("check_blacklist",
    func(args ...any) (bloblang.Function, error) {
        pan := args[0].(string)
        return func() (any, error) {
            return !isBlocked(pan), nil
        }, nil
    },
))

// Method style: this.field.my_method()
v, _ := schemix.New(schema, schemix.WithMethod("is_valid_bin",
    func(v any) (any, error) {
        return checkBIN(v.(string)), nil
    },
))

// V2 style with typed parameters (PluginSpec + ParsedParams)
v, _ := schemix.New(schema, schemix.WithFunctionV2("calc_fee",
    bloblang.NewPluginSpec().
        Param(bloblang.NewInt64Param("amount")).
        Param(bloblang.NewFloat64Param("rate")),
    func(args *bloblang.ParsedParams) (bloblang.Function, error) {
        amount, _ := args.GetInt64("amount")
        rate, _ := args.GetFloat64("rate")
        return func() (any, error) { return float64(amount) * rate, nil }, nil
    },
))

// V2 method with params: this.field.method(param: value)
v, _ := schemix.New(schema, schemix.WithMethodV2("in_range",
    bloblang.NewPluginSpec().
        Param(bloblang.NewInt64Param("min")).
        Param(bloblang.NewInt64Param("max")),
    func(args *bloblang.ParsedParams) (bloblang.Method, error) {
        min, _ := args.GetInt64("min")
        max, _ := args.GetInt64("max")
        return func(v any) (any, error) {
            n := v.(int64)
            return n >= min && n <= max, nil
        }, nil
    },
))

FuncMap (Reusable Collections)

For multiple custom functions, use FuncMap to build once and share:

funcs := schemix.NewFuncMap(
    schemix.Func("check_blacklist", blacklistFn),
    schemix.Func("calc_fee", feeFn),
    schemix.Method("mask_pan", maskFn),
    schemix.MethodV2("in_range", rangeSpec, rangeCtor),
)

// Share across validators
v1, _ := schemix.New(schema1, schemix.WithFuncMap(funcs))
v2, _ := schemix.New(schema2, schemix.WithFuncMap(funcs))

Names are validated at construction time (must be snake_case: /^[a-z0-9]+(_[a-z0-9]+)*$/).

Overriding Built-in Validators

Built-in names are protected by default. Use WithOverrideMethod or WithOverrideFunc to explicitly replace them:

// Override a specific built-in method
v, _ := schemix.New(schema,
    schemix.WithOverrideMethod("is_email"),
    schemix.WithMethod("is_email", myStrictEmailFn),
)

// Override a specific built-in function
v, _ := schemix.New(schema,
    schemix.WithOverrideFunc("is_valid_date"),
    schemix.WithFunction("is_valid_date", myDateFn),
)

// Override all — disable conflict checks entirely
v, _ := schemix.New(schema, schemix.WithOverrideAll(), schemix.WithFuncMap(myFuncs))

Note: Function and Method are separate namespaces. Registering a Function named is_email does NOT conflict with the built-in Method is_email.

Error Handling

r := v.Process(data)

r.Valid                              // bool
r.Err()                              // combined error (nil if valid)
r.FirstError()                       // *ValidationError
r.ErrorsByPath("pan")                // []ValidationError
r.ErrorsByCode(schemix.CodeTypeMismatch) // []ValidationError
r.ErrorsByType("cue")                // []ValidationError — filter by layer
r.HasCode(schemix.CodeBizRuleFailed) // bool — quick category check
r.HasErrorsAt("email")              // bool — field-level check
r.ErrorMessages()                    // newline-joined string

Each ValidationError carries:

Field Meaning
Code Stable error code (E1E01, E2B01, …)
Path Field path — items[0].price, order.customer.age
Type Layer that produced it — cue, bloblang, meta
FieldType Schema type of the field — string, int, list… (empty when not applicable)
Message Raw diagnostic: CUE/Bloblang wording, for logs
Suggestion Closest valid value — enum violations only

Enum errors name every accepted value and suggest the closest match:

r := v.Process(map[string]any{"currency": "USE"}) // schema: "CNY" | "USD" | "EUR"

r.Errors[0].Message           // value "USE" not in enum ["CNY", "USD", "EUR"]
r.Errors[0].Suggestion        // USD
r.Errors[0].FriendlyMessage() // currency must be one of ["CNY", "USD", "EUR"] — did you mean "USD"?

Suggestion is populated for enums only. A range or regex violation has no meaningful value to guess, and inventing one would mislead — the bound is already in the message (value 999 out of bound <=150).

Custom Error Messages

Message and FriendlyMessage() are both always available, which covers the two audiences without a mode switch:

log.Warn(e.Message)                    // raw: age: conflicting values "old" and int
render(e.FriendlyMessage())            // user-facing: age must be of type int

FriendlyMessage() is derived from the structured fields (Code, Path, FieldType, Suggestion), never returns an empty string, and keeps CUE internals out of user-visible text.

For i18n or full control, provide an ErrorFormatter — it replaces Message:

v := schemix.MustNew(schema, schemix.WithErrorFormatter(
    func(code schemix.ErrorCode, path, detail string) string {
        return i18n.T("zh-CN", string(code), path)
    },
))

The formatter receives the error code, field path, and default detail message. With no formatter, Message carries the raw CUE/Bloblang text.

Schema Composition

Use NewFromValue to build validators from pre-compiled CUE values with shared definitions:

ctx := cuecontext.New()
schema := ctx.CompileString(`{
    #PAN:      =~"^[0-9]{16}$"
    #Amount:   int & >0
    #Currency: "CNY" | "USD" | "EUR"

    pan:      #PAN
    amount:   #Amount
    currency: #Currency
}`)

v, err := schemix.NewFromValue(schema)

Definitions carry constraints only — attributes belong on the fields that reference them:

#PAN: =~"^[0-9]{16}$"

pan: #PAN @blob(this.pan.luhn_valid())   // ✅ rule on the field → error path "pan"
#PAN: =~"^[0-9]{16}$" @blob(this.pan.luhn_valid())   // ❌ New() returns an error

A definition is a reusable template while a @blob expression binds to an absolute path, so a definition referenced by two fields has no single path the expression could resolve against. Such an attribute is never extracted, so New() rejects it rather than validating less than the schema appears to. Attributes on fields inside a struct definition are fine — a reference expands them onto real paths:

#User: { age: int @blob(this.user.age >= 18) }   // ✅ rule path becomes "user.age"
user: #User

Schema Introspection

Inspect schema structure at runtime for documentation or UI generation:

fields := v.Fields() // []FieldInfo

for _, f := range fields {
    fmt.Printf("%s: %s (optional=%v, blob=%v)\n", f.Path, f.Type, f.Optional, f.HasBlob)
    for _, child := range f.Children {
        fmt.Printf("  %s: %s\n", child.Path, child.Type)
    }
}

FailMode

Mode Best For Behavior
FailAll Form validation Collect all errors
FailFast API gateway Stop at first error
FailPriority Layered validation Collect CUE + Blob errors in the first failing priority group; skip higher groups
r := v.ProcessWithMode(data, schemix.FailFast)     // 1 error max
r := v.ProcessWithMode(data, schemix.FailAll)      // all errors
r := v.ProcessWithMode(data, schemix.FailPriority) // first failing group only

Processing contracts: CUE and Blob rules in the same FailPriority group are both evaluated. Once that group fails, higher-priority-number groups do not run. Any invalid result has Output == nil. A non-bool @blob() result must satisfy its field schema or validation fails with E2T01.

Error Codes

Format: E{layer}{category}{seq}

Constant Code Layer Meaning
CodeConfigError E0C01 Config Invalid configuration (e.g. undefined FailMode)
CodeFormatMismatch E1F01 CUE Regex format mismatch
CodeTypeMismatch E1T01 CUE Type error
CodeEnumInvalid E1E01 CUE Invalid enum value
CodeRangeViolation E1R01 CUE Range exceeded
CodeRequiredMissing E1M01 CUE Required field missing
CodeArrayElement E1A01 CUE Array element failed
CodeCUEOther E1X01 CUE Other CUE error
CodeBizRuleFailed E2B01 Blob Business rule false
CodeExprExecError E2X01 Blob Expression error
CodeBlobTypeMismatch E2T01 Blob @blob type contract violation
CodeCondRequired E3C01 Meta Conditional required
CodeMetaRuntimeError E3X01 Meta Meta expression runtime error

Bloblang Integration

reg := schemix.NewRegistry()
reg.Register("payment", cueSrc)
env := bloblang.NewEnvironment()
reg.RegisterAllTo(env) // scoped method + function forms

Method form — validates this:

let r = this.validate_schema(name: "payment", mode: "fast")
let r = this.process_schema(name: "payment", mode: "fast")

Function form — dynamic data source:

let r = validate_schema(data: this.payload, name: "payment")
let r = process_schema(data: this.payload, name: "payment")

validate_schema vs process_schema:

Plugin Returns Use When
validate_schema {valid, errors} You only need pass/fail + error details
process_schema {valid, errors, output} You also need computed field values from @blob()

Registry Management

reg := schemix.NewRegistry()       // shared CUE context internally
reg.Register("user", cueSrc)       // compile + store
reg.Has("user")                    // true
reg.List()                         // ["user"] — sorted
reg.Len()                          // 1
reg.Unregister("user")             // remove

// Scoped Bloblang registration (recommended)
env := bloblang.NewEnvironment()
reg.RegisterAllTo(env)             // register both method + function forms into env
reg.RegisterMethodsTo(env)         // method form only into env
reg.RegisterFunctionsTo(env)       // function form only into env

// Deprecated global registration (uses GlobalEnvironment; repeated registration returns an error)
reg.RegisterAll()                  // register both method + function forms
reg.RegisterMethods()              // method form only: this.validate_schema(...) / this.process_schema(...)
reg.RegisterFunctions()            // function form only: validate_schema(data: ...) / process_schema(data: ...)

Observability

Metrics and tracing are opt-in. When neither is configured, every related code path is skipped — Process and Validate incur zero overhead.

Metrics

Implement MetricsRecorder and attach it with WithMetricsRecorder; WithName labels metrics per schema:

v, _ := schemix.New(schema,
    schemix.WithName("payment"),
    schemix.WithMetricsRecorder(rec),
)
Method Called
ObserveValidation(d, valid, schemaName) once per Process / Validate
ObserveLayerDuration(layer, d, schemaName) once per layer — cue, blob
ObserveErrorCode(code, schemaName) once per validation error
ObserveBlobExecution(path, d, success) once per @blob rule execution
ObserveFastpathDecision(path, hit) once per field holding a fast constraint

Implementations must be concurrency-safe and non-blocking — they run inline on every call. Buffer and batch asynchronously rather than doing network I/O.

Ready-made recorders

Both live in their own module, so they add no dependencies to schemix itself:

go get github.com/mredencom/schemix/schemixprom   # Prometheus
go get github.com/mredencom/schemix/schemixotel   # OpenTelemetry metrics
// Prometheus
rec, err := schemixprom.New(prometheus.DefaultRegisterer,
    schemixprom.WithNamespace("myapp"))

// OpenTelemetry
rec, err := schemixotel.New(otel.GetMeterProvider())

schemixprom registers {namespace}_schemix_*: validation_duration_seconds, validations_total, errors_total, blob_duration_seconds, blob_executions_total, layer_duration_seconds, fastpath_decisions_total. schemixotel emits the same set as schemix.validation.duration / .total, schemix.layer.duration, schemix.blob.duration / .total, schemix.error.total, schemix.fastpath.total.

Tracing

Spans are created only on the context-aware methods:

v, _ := schemix.New(schema, schemix.WithTracerProvider(otel.GetTracerProvider()))

r := v.ProcessContext(ctx, data) // root span + schemix.cue / schemix.blob children

The root span carries schemix.schema_name, schemix.fail_mode, schemix.valid, schemix.error_count and schemix.field_count, and records a validation_error event per error (capped at 20 per span).

Convenience API

// Construction
v := schemix.MustNew(cueSrc)                    // panic on error
v, _ := schemix.NewWithContext(ctx, src)         // shared CUE context
v, _ := schemix.NewFromValue(cueValue)           // from pre-compiled CUE value

// Options — custom functions
schemix.WithErrorFormatter(fn)                   // custom error messages
schemix.WithFunction(name, ctor)                 // custom function (V1)
schemix.WithFunctionV2(name, spec, ctor)         // custom function (V2)
schemix.WithMethod(name, fn)                     // custom method (V1)
schemix.WithMethodV2(name, spec, ctor)           // custom method (V2)
schemix.WithFuncMap(funcs)                       // inject reusable FuncMap
schemix.WithMaxSchemaDepth(32)                   // bound construction-time schema recursion

// Options — override built-in validators
schemix.WithOverrideMethod(names...)             // allow overriding specific built-in methods
schemix.WithOverrideFunc(names...)               // allow overriding specific built-in functions
schemix.WithOverrideAll()                        // disable all conflict checks

// FuncMap construction
funcs := schemix.NewFuncMap(opts...)             // build reusable collection
schemix.Func(name, ctor)                         // FuncMap entry: function (V1)
schemix.FuncV2(name, spec, ctor)                 // FuncMap entry: function (V2)
schemix.Method(name, fn)                         // FuncMap entry: method (V1)
schemix.MethodV2(name, spec, ctor)               // FuncMap entry: method (V2)
funcs.Err()                                      // first validation error (nil if valid)

// Validation (fast path — no Output allocation)
valid, errs := v.Validate(data)

// Processing (validation + computed fields)
r := v.Process(data)
r := v.ProcessWithMode(data, schemix.FailFast)

// Introspection
fields := v.Fields()                             // []FieldInfo

Benchmarks

Apple M4, Go 1.25.11 — 6 fields (3 CUE + 3 @blob):

Operation Time Memory Allocs
New (compile) 430 µs 796 KiB 22366
Process (valid) 4.67 µs 11.90 KiB 86
Process (invalid) 5.59 µs 13.14 KiB 102
Process (nested) 37.35 µs 45.86 KiB 492
Validate (no output) 4.82 µs 11.54 KiB 82
Process (parallel, 10 cores) 4.20 µs 11.90 KiB 86
ValidateFields (fast path) 146.5 ns 0 B 0
Registry.Get 6.25 ns 0 B 0

Simple scalar fields use a Go-native fast path that bypasses CUE entirely, achieving about 175x speedup over the CUE legacy path (146.5ns vs 25.62µs).

cue.Context.Encode is lazy: a schema whose fields are all served by the fast path never converts the input into a cue.Value at all. That is exactly the 39 allocations missing from every row above compared to earlier releases (Process 125 → 86, Validate 121 → 82). Nested and array schemas still require the encode, which is why Process (nested) is unchanged at 492.

Pull requests also run base and head benchmarks on the same CI runner. A statistically significant regression above 5% fails the benchmark gate.

Comparison

All engines validate the same five constraints (pan 16 digits, amount int > 0, currency enum, age 0..150, email format), each in that library's idiomatic best form. An equivalence test asserts all six reach the identical verdict before any number is published.

Apple M4, Go 1.25.11, benchstat medians. Time / allocations per operation:

Scenario schemix go-playground/validator ozzo-validation jsonschema v6 raw CUE API
Scalar, valid 382 ns · 0 784 ns · 6 1.72 µs · 37 1.87 µs · 56 12.86 µs · 186
Scalar, invalid 1.06 µs · 15 733 ns · 25 2.05 µs · 49 2.13 µs · 81 16.30 µs · 301
Parallel, 10 cores ~100 ns · 0 247 ns · 6 785 ns · 56
JSON bytes, end-to-end 1.69 µs · 31 1.50 µs · 14 2.52 µs · 45 2.82 µs · 80
Nested + 3-item array 27.35 µs · 432 1.05 µs · 10 4.35 µs · 133
With one @blob() rule 6.44 µs · 127 not supported not supported not supported
Compile (once at startup) 43.97 µs 10.11 µs 65.97 µs

Capabilities, where the difference is structural rather than a matter of nanoseconds:

schemix validator ozzo JSON Schema
Schema is hot-loadable text, not compiled Go
Computed / derived output fields
Dynamic expression language ✅ Bloblang ⚠️ fixed tags ✅ Go ⚠️ if/then
Stable structured error codes
Priority-grouped failure isolation
Metrics + OTel tracing hooks
Cross-language portability

Scalar-only schemas validate allocation-free — 2.1x faster than struct-tag reflection and 34x faster than driving CUE directly, because cue.Context.Encode is skipped entirely when every field is served by the Go-native fast path.

Two honest boundaries on that headline:

  • Add one @blob() rule, a nested struct, or an array and the input must be encoded into a cue.Value — cost jumps by an order of magnitude.
  • Arrays are the weak spot: the fast path has no list descriptor, so the whole list goes to cue.Value.Unify at ~6.3 µs per element. Validating large collections element-by-element against a per-element Validator is measured 62-82x faster.

What schemix offers that raw throughput does not cover: the schema is hot-loadable text rather than compiled Go, plus computed fields (@blob()), dynamic expressions, structured error codes, priority-grouped failure isolation, metrics/OTel hooks, and a Benthos pipeline plugin. If you need none of those and the shape is a compile-time Go struct, go-playground/validator is the leaner choice; if the schema must be portable across languages, use JSON Schema.

Full per-scenario tables, the array breakdown, and reproduction steps: benchmarks/comparison.

License

MIT

About

Schema-driven validation & transformation engine — CUE constraints + Bloblang expressions, unified in Go.

Topics

Resources

Code of conduct

Contributing

Security policy

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Used by

Contributors

Languages