Nette Schema: validating and normalizing PHP data in one pass
📐 Validating data structures against a given Schema.
At a glance
- What is it?
- Nette Schema is a PHP library that checks a data structure against a schema and returns the normalized result, with a schema written as code through the Expect API rather than as a separate document, so defaults, casts and type rules live in the same place as the rest of your application. It suits anyone handling request payloads or configuration in PHP, and it asks for PHP 8.1 or later.
- Who is it for?
- Adopt Nette Schema if you accept untyped input in PHP, from an API request or a configuration file, and want one pass that both rejects what is wrong and converts what is right, because the normalization is the part that usually gets skipped. Do not adopt it where a JSON Schema document is required by a contract you do not control, since the schema here is PHP code.
- Can I use it commercially?
- Check first. The repository uses a licence we do not classify automatically, so read its LICENSE file before any commercial use.
- Is it still maintained?
- Yes. The repository last received commits 1 day ago.
- What is it written in?
- Mainly PHP, according to GitHub's language statistics.
Answers come from the project's GitHub data, last synced on October 23, 2026, and from our analysis. They are not legal advice.
Editorial analysis
One pass that validates and normalizes
Nette Schema is a practical library for validating and normalizing data structures against a given schema with a smart, easy-to-understand API, and it is part of the Nette family of PHP libraries.
The word normalizing is the one that matters. A validator tells you that something is wrong. A normalizer tells you that something is wrong and hands you the corrected value. The library does both in the same pass, and the corrected value is the thing your application actually wants.
That distinction shows up in the simplest possible example. If a form sends a boolean as the string 1, a validator reports a type error, and the developer has to write the conversion themselves. With a schema that lists the accepted values and asks for a cast, the same input comes back as a boolean, and the conversion rule is written in one place that the schema owns.
The data being validated is described as anything you would want to check, data submitted by a user through an API, a configuration file, and so on. There is no adapter, no annotation scanning and no separate definition file, because the schema is code.
The package is on Packagist and installs with Composer, it requires PHP 8.1 and supports PHP up to 8.5, and the documentation is on the Nette documentation site. The releases are steady, with 1.3.6 in August 2026, 1.3.5 in February 2026 and 1.3.3 in October 2025, and the last push to the repository was on 2026-10-14.
The Processor and the exception contract
The whole library is driven by one class, the Processor, and the contract is short enough to remember.
$processor = new Nette\Schema\Processor;
try {
$normalized = $processor->process($schema, $data);
} catch (Nette\Schema\ValidationException $e) {
echo 'Data is invalid: ' . $e->getMessage();
}You construct a processor, you call process with the schema and the data, and you get back either the normalized value or an exception. There is no result object to check and no boolean to test.
The exception carries two levels of detail. The getMessages method returns an array of all the messages as strings, and the getMessageObjects method returns all the messages as Message objects.
That second method is the one that matters for an application that has to report errors properly. A string is fine for a log line, but a message object is a structured thing with a path to the offending element, which is what you need to attach an error to a form field. Returning both from one exception means the caller can choose the level of detail it wants rather than parsing strings to recover it.
The processor is reusable across schemas and data, which is why it is constructed once and passed around, and the same instance is used in every example in the documentation.
Defining expectations with Expect::structure
The schema is defined with the Expect class, and the documentation is careful to present it as a description of expectations rather than as configuration.
use Nette\Schema\Expect;
$schema = Expect::structure([
'processRefund' => Expect::bool(),
'refundAmount' => Expect::int(),
]);The example is a refund request with a boolean flag and an integer amount, and the data goes through the same processor.
$data = [
'processRefund' => true,
'refundAmount' => 17,
];
$normalized = $processor->process($schema, $data); // OK, passes validationTwo properties of the output are worth noting. The result is a stdClass object, and if you want an array instead you add the castTo('array') call to the schema. That is a deliberate choice, an object by default, because the usual consumer of a normalized structure is code that accesses properties, and because an object cannot accidentally be indexed by a typo in the same way an array can.
The documentation also states its position on the API design, that the schema definition looks understandable even if you are seeing it for the first time. That is a claim worth checking against your own experience,
Optional, required, nullable and the default that surprises people
The behaviour of structure items is where the documentation spends most of its words, and one rule in particular catches people out.
All elements of the structure are optional and have a default value of null. So a data array missing the boolean still passes validation, and the output contains the key with a null value.
$data = [
'refundAmount' => 17,
];
$normalized = $processor->process($schema, $data); // OK, passes validation
// $normalized = {'processRefund' => null, 'refundAmount' => 17}And here is the part that surprises people. The fact that the default value is null does not mean the input would accept a null for that field. No, the input must be a boolean, true or false only, and to allow null you have to say so explicitly with the nullable call.
Expect::bool()->nullable()The distinction is between what the schema produces when a value is absent and what it accepts when a value is present, and keeping them separate is the point. A field that defaults to null is a field that is optional, and a field that accepts null is a field whose type includes null, and a schema that conflates them would let a client send null through to code that expects a boolean.
The other modifiers follow the same pattern. An item becomes mandatory with the required call, and the default value changes with the default call, for example false, or with the shorthand form Expect::bool(false) which passes the default directly.
The normalization case then builds on all of it, where a boolean field should also accept 1 and 0.
$schema = Expect::structure([
'processRefund' => Expect::anyOf(true, false, 1, 0)->castTo('bool'),
'refundAmount' => Expect::int(),
]);
$normalized = $processor->process($schema, $data);
is_bool($normalized->processRefund); // trueTypes, unions and class names
The type declarations cover everything PHP can express, and the list is short enough to be quoted.
Expect::string($default = null)
Expect::int($default = null)
Expect::float($default = null)
Expect::bool($default = null)
Expect::null()
Expect::array($default = [])
Expect::list($default = [])The default is always null, with the exception of array and list, where it is an empty array. A list is defined in the documentation as an array indexed by a sequence of numeric keys starting from zero, which is to say a non-associative array.
Beyond the built-in types, the schema can use every type supported by the Nette Validators class, for example Expect::type('scalar') with the shorthand Expect::scalar(), and it can name a class or an interface directly, such as Expect::type('AddressEntity').
Unions have their own syntax.
Expect::type('bool|string|array')So the type system is not a closed set of the seven built-ins, it is the union of PHP's own type syntax with the validators' vocabulary, and a class name is a type. That means the schema can describe an object graph, not just a tree of scalars, which is the difference between validating a request body and validating a configuration file that names a service class.
The cast mechanism is what makes the union useful in practice, because anyOf with a cast turns a set of accepted inputs into one canonical output type.
arrayOf, listOf, and the difference a list makes
An array is too general a structure to be useful in a schema, and the library gives you two ways to say more about it.
The first is the array of values, where you specify what the elements may contain.
$schema = Expect::arrayOf('string');
$processor->process($schema, ['hello', 'world']); // OK
$processor->process($schema, ['a' => 'hello', 'b' => 'world']); // OK
$processor->process($schema, ['key' => 123]); // ERROR: 123 is not a stringThe second parameter specifies the keys, and that has been available since version 1.2.
$schema = Expect::arrayOf('string', 'int');
$processor->process($schema, ['hello', 'world']); // OK
$processor->process($schema, ['a' => 'hello']); // ERROR: 'a' is not an intSo you can say that the values are strings and the keys are integers, and the check is on both halves of each pair.
The list is stricter, and the examples show the three ways it can fail.
$schema = Expect::listOf('string');
$processor->process($schema, ['a', 'b']); // OK
$processor->process($schema, ['a', 123]); // ERROR: 123 is not a string
$processor->process($schema, ['key' => 'a']); // ERROR: not a list
$processor->process($schema, [1 => 'a', 0 => 'b']); // ERROR: also not a listThe last case is the interesting one. The keys are the right values in the wrong order, so the array is not a list even though every element is a string, because a list requires the keys to be a sequence starting from zero.
Two conveniences round it out. The wrap parameter, available since version 1.4, accepts a single value and wraps it into a one-item list, and the element type can itself be a schema, so Expect::arrayOf(Expect::bool()) nests expectations rather than only names of types.
Editorial conclusion
Adopt Nette Schema if you accept untyped input in PHP, from an API request or a configuration file, and want one pass that both rejects what is wrong and converts what is right, because the normalization is the part that usually gets skipped. Do not adopt it where a JSON Schema document is required by a contract you do not control, since the schema here is PHP code. Before you rely on it, check the PHP version, because the library requires 8.1 and supports up to 8.5, and read the documentation for the nullable and default rules, since a default of null does not mean null is accepted as input.
Frequently asked questions
What does Nette Schema do?
It is a library for validating and normalizing data structures against a given schema with a schema written in PHP through the Expect API. The Processor returns normalized data or throws a ValidationException, and the exception offers both string messages and structured Message objects.
How do I install Nette Schema and what PHP version does it need?
It installs with composer require nette/schema, requires PHP version 8.1 and supports PHP up to 8.5. The documentation is on the Nette documentation site.
Does a null default mean the field accepts null?
No. All elements of a structure are optional and default to null, but the input must still be a valid value for the type, so a boolean field rejects null unless you add the nullable call explicitly, for example Expect::bool()->nullable().
How do I express arrays and lists in a Nette Schema?
Expect::arrayOf specifies the allowed element types and, since version 1.2, a second parameter for the key types. Expect::listOf is stricter, requiring an indexed array whose keys form a sequence from zero, and it accepts a wrap parameter since version 1.4 so a single value is accepted and wrapped into a one-item list.
Official sources
Add this badge to your README
If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.
[](https://hysenlabs.com/projects/nette-schema)Was this useful?