AnyVali Ruby SDK Reference¶
Installation¶
Install via RubyGems:
gem install anyvali
Or add to your Gemfile:
gem 'anyvali'
Then run:
bundle install
Requires Ruby 3.1 or later.
Quick Start¶
require 'anyvali'
# Define a schema
user_schema = AnyVali.object(
properties: {
id: AnyVali.int64,
name: AnyVali.string.min_length(1).max_length(100),
email: AnyVali.string.format("email"),
age: AnyVali.optional(AnyVali.int_.min(0).max(150)),
role: AnyVali.enum_("admin", "user", "guest"),
},
required: [:id, :name, :email]
)
# Parse input (raises ValidationError on failure)
user = user_schema.parse({
id: 42,
name: "Alice",
email: "alice@example.com",
role: "admin",
})
# => { id: 42, name: "Alice", email: "alice@example.com", role: "admin" }
# Safe parse (never raises)
result = user_schema.safe_parse({ id: "not-a-number", name: "" })
if result.failure?
result.issues.each do |issue|
puts "#{issue.path.join('.')}: [#{issue.code}] #{issue.message}"
end
end
Schema Builders¶
All schemas are created through module methods on AnyVali. Each method returns a schema instance that can be refined with chained constraints.
Special Types¶
any_schema = AnyVali.any # accepts any value
unknown_schema = AnyVali.unknown # accepts any value, requires explicit handling
never_schema = AnyVali.never # rejects all values
Primitives¶
str = AnyVali.string
num = AnyVali.number # IEEE 754 float64
bool = AnyVali.bool
null = AnyVali.null
Numeric Types¶
AnyVali provides sized integer and float types for cross-language safety:
# Default integer (int64)
int = AnyVali.int_
# Signed integers
i8 = AnyVali.int8 # -128 to 127
i16 = AnyVali.int16 # -32,768 to 32,767
i32 = AnyVali.int32 # -2^31 to 2^31-1
i64 = AnyVali.int64 # -2^63 to 2^63-1
# Unsigned integers
u8 = AnyVali.uint8 # 0 to 255
u16 = AnyVali.uint16 # 0 to 65,535
u32 = AnyVali.uint32 # 0 to 2^32-1
u64 = AnyVali.uint64 # 0 to 2^64-1
# Floats
f32 = AnyVali.float32
f64 = AnyVali.float64 # same as number
Note that int_ uses a trailing underscore to avoid conflicting with Ruby's built-in Integer conversion method.
Literal and Enum¶
# Literal matches a single exact value
active = AnyVali.literal("active")
zero = AnyVali.literal(0)
yes = AnyVali.literal(true)
# Enum matches one of several allowed values
status = AnyVali.enum_("pending", "active", "disabled")
Note that enum_ uses a trailing underscore because enum is not a Ruby keyword but avoids potential naming conflicts.
Arrays and Tuples¶
# Homogeneous array of strings
tags = AnyVali.array(AnyVali.string)
# Array with length constraints
top_three = AnyVali.array(AnyVali.string)
.min_items(1)
.max_items(3)
# Tuple with fixed positional types
coordinate = AnyVali.tuple(
AnyVali.float64,
AnyVali.float64,
)
Objects¶
# Basic object with required and optional fields
user = AnyVali.object(
properties: {
name: AnyVali.string,
email: AnyVali.string.format("email"),
age: AnyVali.int_,
},
required: [:name, :email]
)
# Unknown key handling
strict = AnyVali.object(
properties: { name: AnyVali.string },
required: [:name],
unknown_keys: "reject" # error on unexpected keys (default)
)
stripped = AnyVali.object(
properties: { name: AnyVali.string },
required: [:name],
unknown_keys: "strip" # silently remove unexpected keys
)
passthrough = AnyVali.object(
properties: { name: AnyVali.string },
required: [:name],
unknown_keys: "allow" # keep unexpected keys as-is
)
Records¶
A record validates a hash where all values conform to a single schema:
# Map of string keys to integer values
scores = AnyVali.record(AnyVali.int_)
scores.parse({ "alice" => 95, "bob" => 87 })
# => { "alice" => 95, "bob" => 87 }
Composition¶
# Union: value must match at least one variant
string_or_int = AnyVali.union(
AnyVali.string,
AnyVali.int_,
)
# Intersection: value must match all schemas
named_and_aged = AnyVali.intersection(
AnyVali.object(properties: { name: AnyVali.string }, required: [:name]),
AnyVali.object(properties: { age: AnyVali.int_ }, required: [:age]),
)
Modifiers¶
# Optional: value may be absent (field can be omitted)
maybe_age = AnyVali.optional(AnyVali.int_)
# Nullable: value may be nil
nullable_name = AnyVali.nullable(AnyVali.string)
# Combine both: field can be omitted, or present as nil or string
optional_nullable_name = AnyVali.optional(
AnyVali.nullable(AnyVali.string)
)
References¶
References allow recursive and reusable schema definitions:
tree_node = AnyVali.object(
properties: {
value: AnyVali.string,
children: AnyVali.array(AnyVali.ref("#/definitions/TreeNode")),
},
required: [:value]
)
String Constraints¶
All string constraints return the schema instance for chaining:
schema = AnyVali.string
.min_length(1) # minimum character count
.max_length(255) # maximum character count
.pattern(/^[A-Z]/) # regex pattern the value must match
.starts_with("Hello") # value must start with this prefix
.ends_with("!") # value must end with this suffix
.includes("world") # value must contain this substring
.format("email") # named format (email, uri, uuid, date, etc.)
Each constraint can be used independently:
slug = AnyVali.string
.pattern(/\A[a-z0-9]+(?:-[a-z0-9]+)*\z/)
.min_length(1)
.max_length(100)
email = AnyVali.string.format("email")
url = AnyVali.string.format("uri")
uuid = AnyVali.string.format("uuid")
Number Constraints¶
Number constraints apply to all numeric types (number, int_, int8--int64, uint8--uint64, float32, float64):
price = AnyVali.float64
.min(0) # value >= 0
.max(999_999.99) # value <= 999_999.99
.multiple_of(0.01) # must be a multiple of 0.01
rating = AnyVali.int_
.min(1)
.max(5)
temperature = AnyVali.float64
.exclusive_min(-273.15) # value > -273.15
.exclusive_max(1000.0) # value < 1000.0
Array Constraints¶
tags = AnyVali.array(AnyVali.string)
.min_items(1) # at least 1 element
.max_items(10) # at most 10 elements
Coercion¶
Coercion transforms the input value before validation. It runs only when the value is present. Call .coerce(config) on any schema to enable coercion.
Available Coercions¶
String to Integer¶
age = AnyVali.int_.coerce(from: "string")
age.parse("42") # => 42 (string coerced to integer)
age.parse(42) # => 42 (already an integer, no coercion needed)
String to Number¶
price = AnyVali.number.coerce(from: "string")
price.parse("3.14") # => 3.14
String to Boolean¶
flag = AnyVali.bool.coerce(from: "string")
flag.parse("true") # => true
flag.parse("false") # => false
flag.parse("1") # => true
flag.parse("0") # => false
Trim Whitespace¶
trimmed = AnyVali.string.coerce(trim: true)
trimmed.parse(" hello ") # => "hello"
Lowercase / Uppercase¶
lower = AnyVali.string.coerce(lower: true)
lower.parse("HELLO") # => "hello"
upper = AnyVali.string.coerce(upper: true)
upper.parse("hello") # => "HELLO"
Transformations can be combined:
normalized = AnyVali.string.coerce(trim: true, lower: true)
normalized.parse(" Hello World ") # => "hello world"
Defaults¶
Defaults fill in missing (absent) values. They run after coercion and before validation. Call .default(value) on any schema.
The default only applies when the value is absent -- if a value is present, it is validated normally. Defaults must be static values (for portability across SDKs).
role = AnyVali.string.default("user")
role.parse(nil) # => "user" (absent value filled)
role.parse("admin") # => "admin"
tags = AnyVali.array(AnyVali.string).default([])
Defaults work with optional fields in objects:
config = AnyVali.object(
properties: {
theme: AnyVali.optional(AnyVali.string.default("light")),
language: AnyVali.optional(AnyVali.string.default("en")),
},
required: []
)
config.parse({})
# => { "theme" => "light", "language" => "en" }
If the default value itself fails validation, a default_invalid issue is produced.
Parsing¶
Throwing Parse¶
parse returns the validated and coerced value. If validation fails, it raises AnyVali::ValidationError:
schema = AnyVali.string.min_length(1)
begin
value = schema.parse("hello")
# => "hello"
rescue AnyVali::ValidationError => e
puts e.message
e.issues.each do |issue|
puts "[#{issue.code}] #{issue.message}"
end
end
Safe Parse¶
safe_parse never raises. It returns a ParseResult with structured success/failure information:
schema = AnyVali.object(
properties: {
name: AnyVali.string.min_length(1),
email: AnyVali.string.format("email"),
},
required: [:name, :email]
)
result = schema.safe_parse({ name: "", email: "not-an-email" })
if result.success?
# result.value contains the parsed data
user = result.value
else
# result.issues contains all validation errors
result.issues.each do |issue|
puts "#{issue.path.join('.')}: [#{issue.code}] #{issue.message}"
# "name: [too_small] String must have at least 1 character"
# "email: [invalid_format] Invalid email format"
end
end
Issue Structure¶
Each issue in a parse result contains:
| Field | Type | Description |
|---|---|---|
code |
String |
Machine-readable error code (e.g., invalid_type, too_small) |
message |
String |
Human-readable error description |
path |
Array |
Path to the invalid value (e.g., ["users", 0, "email"]) |
expected |
String |
What was expected (e.g., string, int64) |
received |
String |
What was received (e.g., nil, true) |
Export and Import¶
Exporting Schemas¶
Any schema can be exported to AnyVali's portable JSON format:
schema = AnyVali.object(
properties: {
id: AnyVali.int64,
name: AnyVali.string.min_length(1).max_length(100),
},
required: [:id, :name]
)
# Export to portable JSON (returns a Hash)
doc = AnyVali.export(schema, mode: :portable)
require 'json'
puts JSON.pretty_generate(doc)
# {
# "anyvaliVersion": "1.0",
# "schemaVersion": "1",
# "root": {
# "kind": "object",
# "properties": {
# "id": { "kind": "int64" },
# "name": { "kind": "string", "minLength": 1, "maxLength": 100 }
# },
# "required": ["id", "name"],
# "unknownKeys": "reject"
# },
# "definitions": {},
# "extensions": {}
# }
Export modes:
:portable-- fails if the schema uses non-portable features. This is the safe default.:extended-- emits the core schema plus language-specific extension namespaces.
Importing Schemas¶
Schemas can be imported from a JSON document produced by any AnyVali SDK:
# Import from a portable JSON document (Hash or JSON string)
imported = AnyVali.import_schema(doc)
# The imported schema works exactly like a locally-built one
user = imported.parse({ id: 1, name: "Bob" })
This enables cross-language schema sharing. A schema authored in TypeScript, Go, or any other supported language can be exported, stored, and imported back into Ruby.
Type Signatures with RBS¶
The Ruby SDK ships RBS type signature files in sig/anyvali.rbs for use with Steep, Sorbet, and other Ruby type checkers.
The signatures provide type information for schema builders and parse results:
# With RBS, type checkers understand that:
# AnyVali.string => StringSchema
# AnyVali.int_ => IntSchema
# schema.parse(x) => validated type
# result.value => validated type when result.success? is true
schema = AnyVali.string.min_length(1)
value = schema.parse("hello") # Steep/Sorbet knows this is String
To use the type signatures with Steep, add to your Steepfile:
target :app do
signature "sig"
check "lib"
library "anyvali"
end
Practical Examples¶
Rails Controller Validation¶
class UsersController < ApplicationController
CREATE_SCHEMA = AnyVali.object(
properties: {
name: AnyVali.string.min_length(1).max_length(200),
email: AnyVali.string.format("email"),
role: AnyVali.enum_("admin", "member", "viewer"),
settings: AnyVali.optional(AnyVali.object(
properties: {
notifications: AnyVali.bool,
theme: AnyVali.enum_("light", "dark"),
},
required: [:notifications, :theme]
)),
},
required: [:name, :email, :role]
)
def create
result = CREATE_SCHEMA.safe_parse(params.to_unsafe_h[:user])
if result.failure?
render json: {
errors: result.issues.map { |i|
{ field: i.path.join("."), message: i.message }
}
}, status: :unprocessable_entity
return
end
user = User.create!(result.value)
render json: user, status: :created
end
end
API Client Response Validation¶
require 'net/http'
require 'json'
require 'anyvali'
PRODUCT_SCHEMA = AnyVali.object(
properties: {
id: AnyVali.int64,
name: AnyVali.string.min_length(1),
price_cents: AnyVali.uint32.min(0),
currency: AnyVali.enum_("USD", "EUR", "GBP"),
tags: AnyVali.array(AnyVali.string).max_items(20),
metadata: AnyVali.optional(AnyVali.record(AnyVali.any)),
},
required: [:id, :name, :price_cents, :currency, :tags]
)
PRODUCT_LIST_SCHEMA = AnyVali.array(PRODUCT_SCHEMA).min_items(0)
uri = URI("https://api.example.com/products")
response = Net::HTTP.get(uri)
data = JSON.parse(response)
products = PRODUCT_LIST_SCHEMA.parse(data)
products.each do |product|
puts "#{product['name']}: #{product['price_cents']} #{product['currency']}"
end
Configuration File Validation¶
require 'yaml'
require 'anyvali'
CONFIG_SCHEMA = AnyVali.object(
properties: {
database: AnyVali.object(
properties: {
host: AnyVali.string,
port: AnyVali.uint16.min(1).max(65535),
name: AnyVali.string.min_length(1),
user: AnyVali.string,
password: AnyVali.string,
pool: AnyVali.optional(AnyVali.uint8.min(1).max(100)),
},
required: [:host, :port, :name, :user, :password]
),
redis: AnyVali.optional(AnyVali.object(
properties: {
url: AnyVali.string.format("uri"),
ttl: AnyVali.int_.min(0),
},
required: [:url, :ttl]
)),
log_level: AnyVali.enum_("debug", "info", "warn", "error", "fatal"),
},
required: [:database, :log_level]
)
raw = YAML.safe_load_file("config/app.yml")
config = CONFIG_SCHEMA.parse(raw)
Cross-Language Schema Sharing¶
require 'net/http'
require 'json'
require 'anyvali'
# Fetch a schema published by a Go service
uri = URI("https://api.example.com/schemas/event")
doc = JSON.parse(Net::HTTP.get(uri))
event_schema = AnyVali.import_schema(doc)
# Validate incoming webhook data
event_schema.parse(webhook_payload)
# Build a local schema and share it
audit_log = AnyVali.object(
properties: {
action: AnyVali.enum_("create", "update", "delete"),
actor_id: AnyVali.int64,
timestamp: AnyVali.string.format("date-time"),
changes: AnyVali.record(AnyVali.any),
},
required: [:action, :actor_id, :timestamp]
)
exported = AnyVali.export(audit_log, mode: :portable)
# Store in a schema registry or send to other services
Common Patterns¶
Validating Environment Variables¶
Use unknown_keys: "strip" when parsing hashes that contain many extra keys you don't care about, like ENV:
env_schema = AnyVali.object(
properties: { "DATABASE_URL" => AnyVali.string },
required: ["DATABASE_URL"],
unknown_keys: "strip"
)
Without "strip", parse would raise with unknown_key issues for every other variable in the environment (PATH, HOME, etc.) because the default mode is "reject".
| Mode | What happens with extra keys |
|---|---|
"reject" (default) |
Parse fails with unknown_key issues |
"strip" |
Extra keys silently removed from output |
"allow" |
Extra keys passed through to output |
Eagerly Evaluated vs Lazy Defaults¶
.default() accepts any value of the correct type. Expressions like Dir.pwd are evaluated immediately when the schema is created and stored as a static value -- this works fine. What AnyVali does not support is lazy proc/lambda defaults that re-evaluate on each parse call. If you need a fresh value on every parse, apply it after:
config_schema = AnyVali.object(
properties: {
"profile" => AnyVali.optional(AnyVali.string.default("default")),
"app_dir" => AnyVali.optional(AnyVali.string),
},
required: ["profile"],
unknown_keys: "strip"
)
config = config_schema.parse(data)
config["app_dir"] ||= Dir.pwd
This keeps the schema fully portable -- the same JSON document can be imported in Go, Python, or any other SDK without relying on language-specific function calls.
API Reference¶
AnyVali (Module Methods)¶
| Method | Returns | Description |
|---|---|---|
AnyVali.string |
StringSchema |
String schema |
AnyVali.number |
NumberSchema |
IEEE 754 float64 schema |
AnyVali.int_ |
IntSchema |
Signed int64 schema |
AnyVali.int8 |
IntSchema |
Signed 8-bit integer |
AnyVali.int16 |
IntSchema |
Signed 16-bit integer |
AnyVali.int32 |
IntSchema |
Signed 32-bit integer |
AnyVali.int64 |
IntSchema |
Signed 64-bit integer |
AnyVali.uint8 |
IntSchema |
Unsigned 8-bit integer |
AnyVali.uint16 |
IntSchema |
Unsigned 16-bit integer |
AnyVali.uint32 |
IntSchema |
Unsigned 32-bit integer |
AnyVali.uint64 |
IntSchema |
Unsigned 64-bit integer |
AnyVali.float32 |
NumberSchema |
32-bit float |
AnyVali.float64 |
NumberSchema |
64-bit float (same as number) |
AnyVali.bool |
BoolSchema |
Boolean schema |
AnyVali.null |
NullSchema |
Null schema |
AnyVali.any |
AnySchema |
Accepts any value |
AnyVali.unknown |
UnknownSchema |
Accepts any value (explicit handling) |
AnyVali.never |
NeverSchema |
Rejects all values |
AnyVali.literal(value) |
LiteralSchema |
Matches a single exact value |
AnyVali.enum_(*values) |
EnumSchema |
Matches one of the given values |
AnyVali.array(items) |
ArraySchema |
Homogeneous array |
AnyVali.tuple(*elements) |
TupleSchema |
Fixed-position typed array |
AnyVali.object(properties:, required:, unknown_keys:) |
ObjectSchema |
Object/hash schema |
AnyVali.record(values) |
RecordSchema |
String-keyed hash with uniform value type |
AnyVali.union(*variants) |
UnionSchema |
Value must match at least one variant |
AnyVali.intersection(*schemas) |
IntersectionSchema |
Value must match all schemas |
AnyVali.optional(schema) |
OptionalSchema |
Value may be absent |
AnyVali.nullable(schema) |
NullableSchema |
Value may be nil |
AnyVali.ref(ref_path) |
RefSchema |
Reference to a definition |
AnyVali.export(schema, mode:) |
Hash |
Export schema to portable JSON |
AnyVali.import_schema(doc) |
Schema |
Import schema from portable JSON |
Schema Instance Methods¶
| Method | Returns | Description |
|---|---|---|
schema.parse(input) |
Object |
Parse and validate; raises ValidationError on failure |
schema.safe_parse(input) |
ParseResult |
Parse and validate; never raises |
StringSchema Constraints¶
| Method | Parameter | Description |
|---|---|---|
.min_length(n) |
Integer |
Minimum character count |
.max_length(n) |
Integer |
Maximum character count |
.pattern(p) |
Regexp |
Regex pattern the value must match |
.starts_with(s) |
String |
Required prefix |
.ends_with(s) |
String |
Required suffix |
.includes(s) |
String |
Required substring |
.format(f) |
String |
Named format (email, uri, uuid, date, date-time, etc.) |
NumberSchema Constraints¶
| Method | Parameter | Description |
|---|---|---|
.min(n) |
Numeric |
Minimum value (inclusive) |
.max(n) |
Numeric |
Maximum value (inclusive) |
.exclusive_min(n) |
Numeric |
Minimum value (exclusive) |
.exclusive_max(n) |
Numeric |
Maximum value (exclusive) |
.multiple_of(n) |
Numeric |
Value must be a multiple of this |
ArraySchema Constraints¶
| Method | Parameter | Description |
|---|---|---|
.min_items(n) |
Integer |
Minimum number of elements |
.max_items(n) |
Integer |
Maximum number of elements |
Object Unknown Keys¶
| Value | Description |
|---|---|
"reject" |
Error on unexpected keys (default) |
"strip" |
Silently remove unexpected keys |
"allow" |
Keep unexpected keys as-is |
ParseResult¶
| Method / Property | Type | Description |
|---|---|---|
result.success? |
Boolean |
Whether parsing succeeded |
result.failure? |
Boolean |
Whether parsing failed |
result.value |
Object |
The parsed value (only meaningful when success? is true) |
result.issues |
Array |
List of validation issues (empty when success? is true) |
ValidationError¶
Inherits from StandardError. Raised by parse on validation failure.
| Method / Property | Type | Description |
|---|---|---|
e.issues |
Array |
List of validation issues |
e.message |
String |
Summary error message |