This guide covers validation of structured data using AssociativeValidator (for associative arrays) and ObjectValidator (for stdClass objects), both supporting schema-based validation.
- Associative Array Validation
- Object Validation
- Schema Definition
- Output Key Remapping
- Passthrough (undeclared keys)
- Type Coercion
- Advanced Features
- Common Patterns
- Error Handling
use Lemmon\Validator\Validator;
// Simple associative array without schema
$validator = Validator::isAssociative();
$result = $validator->validate(['key' => 'value']);
// Result: ['key' => 'value']
$result = $validator->validate([]);
// Result: []
// Null is allowed for optional validators
$result = $validator->validate(null);
// Result: null// Define schema for user data
$userSchema = Validator::isAssociative([
'name' => Validator::isString()->required(),
'age' => Validator::isInt()->min(0)->max(150),
'email' => Validator::isString()->email()
]);
$userData = [
'name' => 'John Doe',
'age' => 30,
'email' => 'john@example.com'
];
$result = $userSchema->validate($userData);
// Result: ['name' => 'John Doe', 'age' => 30, 'email' => 'john@example.com']// Validate stdClass objects with schema
$objectSchema = Validator::isObject([
'name' => Validator::isString(),
'age' => Validator::isInt()->coerce()
]);
$input = (object)[
'name' => 'John Doe',
'age' => '42' // String will be coerced to int
];
$result = $objectSchema->validate($input);
// Result: stdClass object with name='John Doe', age=42// Coerce associative array to stdClass object
$objectSchema = Validator::isObject([
'name' => Validator::isString(),
'age' => Validator::isInt()
])->coerce();
$input = [
'name' => 'Jane Doe',
'age' => 30
];
$result = $objectSchema->validate($input);
// Result: stdClass object with name='Jane Doe', age=30$schema = Validator::isAssociative([
'required_field' => Validator::isString()->required(),
'optional_field' => Validator::isString() // Optional by default
]);
// Valid - optional field can be missing
$result = $schema->validate(['required_field' => 'value']);
// Invalid - required field missing
$schema->validate(['optional_field' => 'value']); // Throws ValidationException$schema = Validator::isAssociative([
'name' => Validator::isString()->required(),
'status' => Validator::isString()->default('active'),
'priority' => Validator::isInt()->default(1)
]);
$input = ['name' => 'John'];
$result = $schema->validate($input);
// Result: ['name' => 'John', 'status' => 'active', 'priority' => 1]Use outputKey() on schema fields to emit validated values under a different key than the input field. This is useful when the input uses snake_case or IDs while the output should use different property names (e.g. for API responses or after transforming IDs to entities).
$schema = Validator::isAssociative([
'service_id' => Validator::isString()->uuid()->outputKey('service'),
'user_id' => Validator::isString()->uuid()->outputKey('user'),
]);
$input = [
'service_id' => '550e8400-e29b-41d4-a716-446655440000',
'user_id' => '6ba7b810-9dad-11d1-80b4-00c04fd430c8',
];
$result = $schema->validate($input);
// Result: ['service' => '550e8400-...', 'user' => '6ba7b810-...']
// Note: 'service_id' and 'user_id' are not in the resultWorks with transform() for ID-to-entity resolution:
$schema = Validator::isAssociative([
'service_id' => Validator::isString()->uuid()
->transform(fn(string $id) => $serviceRepo->find($id))
->outputKey('service'),
]);Works with both Validator::isAssociative() and Validator::isObject().
By default, schema validators only include fields in the result that were either:
- Provided in the input data, or
- Have default values applied
Fields that are not provided and don't have defaults are not included in the result:
$schema = Validator::isAssociative([
'name' => Validator::isString()->required(),
'email' => Validator::isString()->email(), // Optional, no default
'status' => Validator::isString()->default('active'), // Has default
'age' => Validator::isInt(), // Optional, no default
'role' => Validator::isString()->default('user') // Has default
]);
// Only provide name and email
$input = [
'name' => 'John Doe',
'email' => 'john@example.com'
];
$result = $schema->validate($input);
// Result: [
// 'name' => 'John Doe', // Provided in input
// 'email' => 'john@example.com', // Provided in input
// 'status' => 'active', // Default value applied
// 'role' => 'user' // Default value applied
// ]
// Note: 'age' is NOT included (not provided, no default)This behavior ensures that:
- Results accurately reflect the validated data without unexpected properties (unless you opt into
passthrough()below) - Default values are consistently applied when fields are missing
- Required field validation still works (missing required fields cause validation to fail)
Call passthrough() on a schema-backed Validator::isAssociative() or Validator::isObject() to copy input keys that are not declared in the schema onto the validated output without validating them. Declared fields are still run through their validators. Values already written from the schema (including keys produced by outputKey()) are not overwritten by passthrough.
Passthrough runs only after all schema fields validate successfully. Copied values are shallow: nested arrays or objects are not recursively validated.
$schema = Validator::isAssociative([
'name' => Validator::isString()->required(),
])->passthrough();
$input = [
'name' => 'Ann',
'metadata' => ['any' => 'structure'],
'version' => 1,
];
$result = $schema->validate($input);
// Result includes name (validated) plus metadata and version (copied as-is)With an empty schema, passthrough() keeps the entire map (still subject to array/object type rules and coercion on the container itself). That pattern is useful for optional opaque payloads such as API metadata:
$result = Validator::isAssociative([])->passthrough()
->validate(['foo' => 1, 'nested' => ['x' => true]]);
// $result is the same associative shape, unvalidatedFor ObjectValidator, only public properties on the input object are considered for passthrough (same visibility as get_object_vars()).
$userSchema = Validator::isAssociative([
'name' => Validator::isString()->required(),
'profile' => Validator::isAssociative([
'bio' => Validator::isString(),
'website' => Validator::isString()->url()
]),
'preferences' => Validator::isObject([
'theme' => Validator::isString()->in(['light', 'dark'])->default('light'),
'notifications' => Validator::isBool()->default(true)
])->coerce()
]);
$userData = [
'name' => 'John Doe',
'profile' => [
'bio' => 'Software developer',
'website' => 'https://johndoe.dev'
],
'preferences' => [
'theme' => 'dark',
'notifications' => false
]
];
$result = $userSchema->validate($userData);$schema = Validator::isAssociative([
'id' => Validator::isInt()->coerce(), // String to int
'active' => Validator::isBool()->coerce(), // String to bool
'price' => Validator::isFloat()->coerce() // String to float
]);
$input = [
'id' => '123',
'active' => 'true',
'price' => '19.99'
];
$result = $schema->validate($input);
// Result: ['id' => 123, 'active' => true, 'price' => 19.99]$schema = Validator::isAssociative([
'level' => Validator::isInt()->in([3, 5, 8]),
'override' => Validator::isBool()
])->coerceAll(); // Recursively enables coercion for all fields, nested schemas, and array items
$input = [
'level' => '5', // String coerced to int
'override' => 'false' // String coerced to bool
];
$result = $schema->validate($input);
// Result: ['level' => 5, 'override' => false]Scope:
coerceAll()propagates through schema fields, nested schemas, and array item validators. It does not reach into assertion operands -- validators passed tosatisfies(),satisfiesAll(),satisfiesAny(),satisfiesNone(), orcontains(). Callcoerce()on those validators individually when needed.
// AssociativeValidator can coerce objects to arrays
$arraySchema = Validator::isAssociative([
'name' => Validator::isString()
])->coerce();
$object = new stdClass();
$object->name = 'John Doe';
$result = $arraySchema->validate($object);
// Result: ['name' => 'John Doe']
// ObjectValidator can coerce arrays to objects
$objectSchema = Validator::isObject([
'name' => Validator::isString()
])->coerce();
$array = ['name' => 'Jane Doe'];
$result = $objectSchema->validate($array);
// Result: stdClass object with name='Jane Doe'$schema = Validator::isAssociative([
'type' => Validator::isString()->in(['user', 'admin'])->required(),
'permissions' => Validator::isArray()->satisfies(
function ($value, $key, $input) {
// Only require permissions for admin users
if ($input['type'] === 'admin') {
return !empty($value);
}
return true;
},
'Admin users must have permissions'
)
]);$productSchema = Validator::isAssociative([
'name' => Validator::isString()->required()->minLength(3),
'price' => Validator::isFloat()->positive()->multipleOf(0.01),
'category' => Validator::isString()->in(['electronics', 'clothing', 'books']),
'tags' => Validator::isArray()->items(Validator::isString()),
'metadata' => Validator::isAssociative([
'weight' => Validator::isFloat()->positive(),
'dimensions' => Validator::isAssociative([
'length' => Validator::isFloat()->positive(),
'width' => Validator::isFloat()->positive(),
'height' => Validator::isFloat()->positive()
])
])
]);// Validate API request payload
$createUserRequest = Validator::isAssociative([
'username' => Validator::isString()
->required()
->minLength(3)
->maxLength(20)
->pattern('/^[a-zA-Z0-9_]+$/', 'Username can only contain letters, numbers, and underscores'),
'email' => Validator::isString()->email()->required(),
'password' => Validator::isString()->minLength(8)->required(),
'profile' => Validator::isAssociative([
'first_name' => Validator::isString()->required(),
'last_name' => Validator::isString()->required(),
'bio' => Validator::isString()->maxLength(500)
])
]);// Application configuration schema
$configSchema = Validator::isAssociative([
'app' => Validator::isAssociative([
'name' => Validator::isString()->required(),
'debug' => Validator::isBool()->default(false),
'timezone' => Validator::isString()->default('UTC')
]),
'database' => Validator::isAssociative([
'host' => Validator::isString()->required(),
'port' => Validator::isInt()->min(1)->max(65535)->default(3306),
'username' => Validator::isString()->required(),
'password' => Validator::isString()->required(),
'database' => Validator::isString()->required()
]),
'cache' => Validator::isAssociative([
'driver' => Validator::isString()->in(['redis', 'memcached', 'file'])->default('file'),
'ttl' => Validator::isInt()->positive()->default(3600)
])
]);// Contact form validation
$contactFormSchema = Validator::isAssociative([
'name' => Validator::isString()->required()->pipe('trim'),
'email' => Validator::isString()->email()->required(),
'subject' => Validator::isString()->required()->maxLength(100),
'message' => Validator::isString()->required()->minLength(10)->maxLength(1000),
'newsletter' => Validator::isBool()->default(false),
'terms' => Validator::isBool()->satisfies(
fn($value) => $value === true,
'You must accept the terms and conditions'
)
]);use Lemmon\Validator\ValidationException;
$schema = Validator::isAssociative([
'name' => Validator::isString()->required(),
'age' => Validator::isInt()->min(18)
]);
$input = [
'age' => 16 // Invalid: too young
// Missing required 'name' field
];
try {
$schema->validate($input);
} catch (ValidationException $e) {
print_r($e->getErrors());
// Output:
// [
// 'name' => ['Value is required'],
// 'age' => ['Value must be at least 18']
// ]
}$nestedSchema = Validator::isAssociative([
'user' => Validator::isAssociative([
'profile' => Validator::isAssociative([
'email' => Validator::isString()->email()->required()
])
])
]);
$input = [
'user' => [
'profile' => [
'email' => 'invalid-email'
]
]
];
try {
$nestedSchema->validate($input);
} catch (ValidationException $e) {
print_r($e->getErrors());
// Nested error structure reflects the schema structure
}$schema = Validator::isAssociative([
'items' => Validator::isArray()->items(Validator::isInt())
]);
[$valid, $result, $errors] = $schema->tryValidate([
'items' => ['1', 'invalid', '3']
]);
if (!$valid) {
echo "Validation failed:\n";
foreach ($errors as $field => $fieldErrors) {
echo "$field: " . implode(', ', $fieldErrors) . "\n";
}
} else {
echo "Validation successful:\n";
print_r($result);
}For API consumption, you can flatten the nested error structure:
use Lemmon\Validator\ValidationException;
// With exceptions
try {
$schema->validate($input);
} catch (ValidationException $e) {
$flattened = $e->getFlattenedErrors();
// Returns: [
// ['path' => 'name', 'message' => 'Value is required'],
// ['path' => 'user.profile.email', 'message' => 'Value must be a valid email address']
// ]
}
// With tryValidate
[$valid, $data, $errors] = $schema->tryValidate($input);
if (!$valid) {
$flattened = ValidationException::flattenErrors($errors);
// Same format as above
}See the Error Handling Guide for complete documentation on flattened errors.
// Registration form with multiple steps
$step1Schema = Validator::isAssociative([
'email' => Validator::isString()->email()->required(),
'password' => Validator::isString()->minLength(8)->required(),
'password_confirm' => Validator::isString()->satisfies(
function ($value, $key, $input) {
return isset($input['password']) && $value === $input['password'];
},
'Password confirmation must match password'
)
]);
$step2Schema = Validator::isAssociative([
'first_name' => Validator::isString()->required(),
'last_name' => Validator::isString()->required(),
'birth_date' => Validator::isString()->date('Y-m-d')->required()
]);
$step3Schema = Validator::isAssociative([
'company' => Validator::isString(),
'job_title' => Validator::isString(),
'industry' => Validator::isString()->in([
'technology', 'finance', 'healthcare', 'education', 'other'
])
]);$productSchema = Validator::isAssociative([
'basic_info' => Validator::isAssociative([
'name' => Validator::isString()->required()->minLength(3)->maxLength(100),
'description' => Validator::isString()->required()->maxLength(2000),
'sku' => Validator::isString()->required()->pattern('/^[A-Z0-9-]+$/'),
'brand' => Validator::isString()->required()
]),
'pricing' => Validator::isAssociative([
'price' => Validator::isFloat()->positive()->multipleOf(0.01)->required(),
'sale_price' => Validator::isFloat()->positive()->multipleOf(0.01),
'currency' => Validator::isString()->in(['USD', 'EUR', 'GBP'])->default('USD')
]),
'inventory' => Validator::isAssociative([
'stock_quantity' => Validator::isInt()->min(0)->required(),
'track_inventory' => Validator::isBool()->default(true),
'allow_backorder' => Validator::isBool()->default(false)
]),
'shipping' => Validator::isAssociative([
'weight' => Validator::isFloat()->positive(),
'dimensions' => Validator::isAssociative([
'length' => Validator::isFloat()->positive(),
'width' => Validator::isFloat()->positive(),
'height' => Validator::isFloat()->positive()
]),
'shipping_class' => Validator::isString()->in(['standard', 'heavy', 'fragile'])
]),
'seo' => Validator::isAssociative([
'meta_title' => Validator::isString()->maxLength(60),
'meta_description' => Validator::isString()->maxLength(160),
'slug' => Validator::isString()->pattern('/^[a-z0-9-]+$/')
])
]);- Learn about Custom Validation for business logic
- Explore Array Validation for complex array validation scenarios
- Check out Error Handling for structured error management
- See Form Validation Examples for practical examples