Skip to content

AnyVali PHP SDK Reference

Installation

Install via Composer:

composer require anyvali/anyvali

Requires PHP 8.1 or later.

Quick Start

<?php

use AnyVali\AnyVali;
use AnyVali\Schema;

// Define a schema
$userSchema = AnyVali::object([
    'id'    => AnyVali::int64(),
    'name'  => AnyVali::string()->minLength(1)->maxLength(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 (throws ValidationError on failure)
$user = $userSchema->parse([
    'id'    => 42,
    'name'  => 'Alice',
    'email' => 'alice@example.com',
    'role'  => 'admin',
]);
// => ['id' => 42, 'name' => 'Alice', 'email' => 'alice@example.com', 'role' => 'admin']

// Safe parse (never throws)
$result = $userSchema->safeParse(['id' => 'not-a-number', 'name' => '']);
if (!$result->success) {
    foreach ($result->issues as $issue) {
        echo implode('.', $issue->path) . ": [{$issue->code}] {$issue->message}\n";
    }
}

Schema Builders

All schemas are created through static factory methods on the AnyVali class. Each method returns a Schema instance that can be further refined with chained constraints.

Special Types

use AnyVali\AnyVali;

$any     = AnyVali::any();       // accepts any value
$unknown = AnyVali::unknown();   // accepts any value, requires explicit handling
$never   = 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()

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']);

Arrays and Tuples

// Homogeneous array of strings
$tags = AnyVali::array(AnyVali::string());

// Array with length constraints
$topThree = AnyVali::array(AnyVali::string())
    ->minItems(1)
    ->maxItems(3);

// Tuple with fixed positional types
$coordinate = AnyVali::tuple([
    AnyVali::float64(),
    AnyVali::float64(),
]);

Objects

use AnyVali\UnknownKeyMode;

// 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'],
    unknownKeys: UnknownKeyMode::Reject,   // error on unexpected keys
);

$stripped = AnyVali::object(
    properties: ['name' => AnyVali::string()],
    required: ['name'],
    unknownKeys: UnknownKeyMode::Strip,    // silently remove unexpected keys
);

$passthrough = AnyVali::object(
    properties: ['name' => AnyVali::string()],
    required: ['name'],
    unknownKeys: UnknownKeyMode::Allow,    // keep unexpected keys as-is
);

Records

A record validates a dictionary 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
$stringOrInt = AnyVali::union([
    AnyVali::string(),
    AnyVali::int(),
]);

// Intersection: value must match all schemas
$namedAndAged = AnyVali::intersection([
    AnyVali::object(['name' => AnyVali::string()], required: ['name']),
    AnyVali::object(['age' => AnyVali::int()], required: ['age']),
]);

Modifiers

// Optional: value may be absent (field can be omitted)
$maybeAge = AnyVali::optional(AnyVali::int());

// Nullable: value may be null
$nullableName = AnyVali::nullable(AnyVali::string());

// Combine both: field can be omitted, or present as null or string
$optionalNullableName = AnyVali::optional(
    AnyVali::nullable(AnyVali::string())
);

References

References allow recursive and reusable schema definitions:

$treeNode = 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()
    ->minLength(1)           // minimum character count
    ->maxLength(255)         // maximum character count
    ->pattern('/^[A-Z]/u')   // regex pattern the value must match
    ->startsWith('Hello')    // value must start with this prefix
    ->endsWith('!')          // 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-z0-9]+(?:-[a-z0-9]+)*$/')
    ->minLength(1)
    ->maxLength(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(999999.99)       // value <= 999999.99
    ->multipleOf(0.01);    // must be a multiple of 0.01

$rating = AnyVali::int()
    ->min(1)
    ->max(5);

$temperature = AnyVali::float64()
    ->exclusiveMin(-273.15)    // value > -273.15
    ->exclusiveMax(1000.0);    // value < 1000.0

Array Constraints

$tags = AnyVali::array(AnyVali::string())
    ->minItems(1)      // at least 1 element
    ->maxItems(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(null);     // => "user" (absent value filled)
$role->parse('admin');  // => "admin"

$tags = AnyVali::array(AnyVali::string())->default([]);

Defaults work with optional fields in objects:

$config = AnyVali::object([
    '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 throws a ValidationError:

use AnyVali\AnyVali;
use AnyVali\ValidationError;

$schema = AnyVali::string()->minLength(1);

try {
    $value = $schema->parse('hello');
    // => 'hello'
} catch (ValidationError $e) {
    echo $e->getMessage();
    foreach ($e->issues as $issue) {
        echo "[{$issue->code}] {$issue->message}\n";
    }
}

Safe Parse

safeParse() never throws. It returns a ParseResult with structured success/failure information:

$schema = AnyVali::object([
    'name'  => AnyVali::string()->minLength(1),
    'email' => AnyVali::string()->format('email'),
], required: ['name', 'email']);

$result = $schema->safeParse([
    'name'  => '',
    'email' => 'not-an-email',
]);

if ($result->success) {
    // $result->value contains the parsed data
    $user = $result->value;
} else {
    // $result->issues contains all validation errors
    foreach ($result->issues as $issue) {
        echo implode('.', $issue->path) . ": [{$issue->code}] {$issue->message}\n";
        // "name: [too_small] String must have at least 1 character"
        // "email: [invalid_format] Invalid email format"
    }
}

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., null, bool)

Export and Import

Exporting Schemas

Any schema can be exported to AnyVali's portable JSON format:

$schema = AnyVali::object([
    'id'   => AnyVali::int64(),
    'name' => AnyVali::string()->minLength(1)->maxLength(100),
], required: ['id', 'name']);

// Export to portable JSON (associative array)
$doc = $schema->export();

echo json_encode($doc, JSON_PRETTY_PRINT);
// {
//     "anyvaliVersion": "1.0",
//     "schemaVersion": "1",
//     "root": {
//         "kind": "object",
//         "properties": {
//             "id": { "kind": "int64" },
//             "name": { "kind": "string", "minLength": 1, "maxLength": 100 }
//         },
//         "required": ["id", "name"],
//         "unknownKeys": "strip"
//     },
//     "definitions": {},
//     "extensions": {}
// }

Importing Schemas

Schemas can be imported from a JSON document produced by any AnyVali SDK:

// Import from a portable JSON document (associative array or JSON string)
$imported = AnyVali::import($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 PHP.

Type Inference with PHPStan and Psalm

The PHP SDK uses @template phpDoc annotations so that static analysis tools can infer the output type of parsed values:

/** @var Schema<string> */
$name = AnyVali::string();

/** @var Schema<int> */
$age = AnyVali::int();

/** @var Schema<float> */
$price = AnyVali::float64();

/** @var Schema<bool> */
$active = AnyVali::bool();

// parse() return type is inferred by PHPStan/Psalm
$parsedName = $name->parse('Alice');   // PHPStan infers: string
$parsedAge  = $age->parse(25);         // PHPStan infers: int

ParseResult also carries the template type:

$result = $name->safeParse('hello');
if ($result->success) {
    // $result->value is inferred as string by PHPStan/Psalm
    echo strtoupper($result->value);
}

Practical Examples

Form Validation

$contactForm = AnyVali::object([
    'name'    => AnyVali::string()->minLength(1)->maxLength(200),
    'email'   => AnyVali::string()->format('email'),
    'subject' => AnyVali::enum(['support', 'sales', 'feedback']),
    'message' => AnyVali::string()->minLength(10)->maxLength(5000),
], required: ['name', 'email', 'subject', 'message']);

$result = $contactForm->safeParse($_POST);
if (!$result->success) {
    http_response_code(422);
    echo json_encode(['errors' => array_map(
        fn($issue) => [
            'field'   => implode('.', $issue->path),
            'message' => $issue->message,
        ],
        $result->issues,
    )]);
    return;
}

$data = $result->value;
// proceed with validated data

API Request Validation

$createOrderSchema = AnyVali::object([
    'customer_id' => AnyVali::int64(),
    'items' => AnyVali::array(
        AnyVali::object([
            'product_id' => AnyVali::int64(),
            'quantity'    => AnyVali::uint16()->min(1),
            'unit_price'  => AnyVali::float64()->min(0)->multipleOf(0.01),
        ], required: ['product_id', 'quantity', 'unit_price']),
    )->minItems(1),
    'notes' => AnyVali::optional(AnyVali::nullable(AnyVali::string()->maxLength(1000))),
], required: ['customer_id', 'items']);

$body = json_decode(file_get_contents('php://input'), true);

try {
    $order = $createOrderSchema->parse($body);
    // $order is now validated and safe to use
} catch (ValidationError $e) {
    http_response_code(400);
    echo json_encode(['errors' => $e->issues]);
}

Configuration Validation

$configSchema = AnyVali::object([
    'database' => AnyVali::object([
        'host'     => AnyVali::string(),
        'port'     => AnyVali::uint16()->min(1)->max(65535),
        'name'     => AnyVali::string()->minLength(1),
        'user'     => AnyVali::string(),
        'password' => AnyVali::string(),
    ], required: ['host', 'port', 'name', 'user', 'password']),
    'cache' => AnyVali::object([
        'driver' => AnyVali::enum(['redis', 'memcached', 'file']),
        'ttl'    => AnyVali::int()->min(0),
    ], required: ['driver', 'ttl']),
    'debug' => AnyVali::optional(AnyVali::bool()),
], required: ['database', 'cache']);

$config = $configSchema->parse(json_decode(
    file_get_contents(__DIR__ . '/config.json'),
    true,
));

Cross-Language Schema Sharing

// Receive a schema from a TypeScript service
$response = file_get_contents('https://api.example.com/schemas/user');
$doc = json_decode($response, true);
$userSchema = AnyVali::import($doc);

// Validate incoming data against the shared schema
$result = $userSchema->safeParse($incomingData);

// Build a local schema and share it back
$auditEvent = AnyVali::object([
    'action'    => AnyVali::enum(['create', 'update', 'delete']),
    'actor_id'  => AnyVali::int64(),
    'timestamp' => AnyVali::string()->format('date-time'),
    'payload'   => AnyVali::record(AnyVali::any()),
], required: ['action', 'actor_id', 'timestamp']);

$exportedDoc = $auditEvent->export();
// Send $exportedDoc to a schema registry for other services to consume

Common Patterns

Validating Configuration Files

Use UnknownKeyMode::Strip when parsing arrays that contain many extra keys you don't care about, like config files with additional entries:

$envSchema = AnyVali::object(
    ['DATABASE_URL' => AnyVali::string()],
    ['DATABASE_URL'],
    UnknownKeyMode::Strip
);

Strip is the default, so this option is only needed when you want to be explicit.

Mode What happens with extra keys
Strip (default) Extra keys silently removed from output
Reject Parse fails with unknown_key issues
Allow Extra keys passed through to output

Eagerly Evaluated vs Lazy Defaults

->default() accepts any value of the correct type. Expressions like getcwd() are evaluated immediately when the schema is created and stored as a static value -- this works fine. What AnyVali does not support is lazy callable defaults that re-evaluate on each parse call. If you need a fresh value on every parse, apply it after:

$configSchema = AnyVali::object(
    [
        'profile' => AnyVali::optional(AnyVali::string()->default('default')),
        'appDir'  => AnyVali::optional(AnyVali::string()),
    ],
    ['profile'],
    UnknownKeyMode::Strip
);

$config = $configSchema->parse($data);
if (!isset($config['appDir'])) {
    $config['appDir'] = getcwd();
}

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 (Static Factory)

Method Returns Description
AnyVali::string() Schema<string> String schema
AnyVali::number() Schema<float> IEEE 754 float64 schema
AnyVali::int() Schema<int> Signed int64 schema
AnyVali::int8() Schema<int> Signed 8-bit integer
AnyVali::int16() Schema<int> Signed 16-bit integer
AnyVali::int32() Schema<int> Signed 32-bit integer
AnyVali::int64() Schema<int> Signed 64-bit integer
AnyVali::uint8() Schema<int> Unsigned 8-bit integer
AnyVali::uint16() Schema<int> Unsigned 16-bit integer
AnyVali::uint32() Schema<int> Unsigned 32-bit integer
AnyVali::uint64() Schema<int> Unsigned 64-bit integer
AnyVali::float32() Schema<float> 32-bit float
AnyVali::float64() Schema<float> 64-bit float (same as number())
AnyVali::bool() Schema<bool> Boolean schema
AnyVali::null() Schema<null> Null schema
AnyVali::any() Schema<mixed> Accepts any value
AnyVali::unknown() Schema<mixed> Accepts any value (explicit handling)
AnyVali::never() Schema<never> Rejects all values
AnyVali::literal($value) Schema Matches a single exact value
AnyVali::enum($values) Schema Matches one of the given values
AnyVali::array($items) Schema<array> Homogeneous array
AnyVali::tuple($elements) Schema<array> Fixed-position typed array
AnyVali::object($properties, $required, $unknownKeys) Schema<array> Object/associative array
AnyVali::record($valueSchema) Schema<array> String-keyed map with uniform value type
AnyVali::union($variants) Schema Value must match at least one variant
AnyVali::intersection($schemas) Schema Value must match all schemas
AnyVali::optional($schema) Schema Value may be absent
AnyVali::nullable($schema) Schema Value may be null
AnyVali::ref($ref) Schema Reference to a definition
AnyVali::import($source) Schema Import schema from portable JSON

Schema Instance Methods

Method Returns Description
$schema->parse($input) mixed Parse and validate; throws ValidationError on failure
$schema->safeParse($input) ParseResult Parse and validate; never throws
$schema->export() array Export to portable JSON document

StringSchema Constraints

Method Parameter Description
->minLength($n) int Minimum character count
->maxLength($n) int Maximum character count
->pattern($p) string Regex pattern the value must match
->startsWith($s) string Required prefix
->endsWith($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) int\|float Minimum value (inclusive)
->max($n) int\|float Maximum value (inclusive)
->exclusiveMin($n) int\|float Minimum value (exclusive)
->exclusiveMax($n) int\|float Maximum value (exclusive)
->multipleOf($n) int\|float Value must be a multiple of this

ArraySchema Constraints

Method Parameter Description
->minItems($n) int Minimum number of elements
->maxItems($n) int Maximum number of elements

UnknownKeyMode (Enum)

Value Description
UnknownKeyMode::Strip Silently remove unexpected keys (default)
UnknownKeyMode::Reject Error on unexpected keys
UnknownKeyMode::Allow Keep unexpected keys as-is

ParseResult

Property Type Description
$result->success bool Whether parsing succeeded
$result->value mixed The parsed value (only meaningful when success is true)
$result->issues array List of validation issues (empty when success is true)

ValidationError

Extends \RuntimeException. Thrown by parse() on validation failure.

Property Type Description
$e->issues array List of validation issues
$e->getMessage() string Summary error message