PHP TypeLang Help

Metadata Configuration

Metadata allows you to configure rules for processing objects and supplementing information. There are several metadata formats, but their capabilities are almost identical. You can choose any format that suits you best, such as PHP Attributes, configuration file in YAML format, or any other format.

Class Metadata

There are several rules that may apply to a specific class. Detailed information on these rules is provided below.

Normalize As Array

This option is responsible for converting an object to an associative array during serialization.

  • In case of the true value specified, the object will be converted to an associative array during normalization

  • In case of the false value specified, the object will be converted to an object (instance of StdClass) during normalization

  • If you omit this option, the default value specified in the configuration will be used

use TypeLang\Mapper\Mapping\NormalizeAsArray; #[NormalizeAsArray] final class WithArrayNormalization { // ... } #[NormalizeAsArray(enabled: false)] final class WithoutArrayNormalization { // ... }
// App.Example.WithArrayNormalization.php return [ 'normalize_as_array' => true, // ... ];
// App.Example.WithoutArrayNormalization.php return [ 'normalize_as_array' => false, // ... ];
// App.Example.WithArrayNormalization.json { "normalize_as_array": true, // ... }
// App.Example.WithoutArrayNormalization.json { "normalize_as_array": false, // ... }
# App.Example.WithArrayNormalization.yaml normalize_as_array: true # ...
# App.Example.WithoutArrayNormalization.yaml normalize_as_array: false # ...
# App.Example.WithArrayNormalization.neon normalize_as_array: true # ...
# App.Example.WithoutArrayNormalization.neon normalize_as_array: false # ...

Discriminator Map

A map discriminator is a set of rules for inferring a type based on the value of a field.

For example, we have the following class hierarchy.

abstract class UserInfo { public function __construct( public string $name, ) {} } final class AdminUserInfo extends UserInfo {} final class GuestUserInfo extends UserInfo {}

In this case, when trying to denormalize the class UserInfo, an error will occur since there will be an attempt to create an abstract class.

$result = (new TypeLang\Mapper\Mapper()) ->denormalize([ 'name' => 'Kirill', ], UserInfo::class); // TypeLang\Mapper\Exception\Runtime\NonInstantiatableException: // Unable to instantiate "UserInfo" of UserInfo{name: string}

А similar error will occur when instantiating an interface.

To solve this problem, there is a discriminator map, with the help of which you can specify which class the data belongs to.

For example, we want that:

  • When passing a 'type' => 'admin', an object AdminUserInfo is created

  • When passing a 'type' => 'guest', an object GuestUserInfo is created

// Expects object(AdminUserInfo) $result = $mapper->denormalize([ 'name' => 'Kirill', 'type' => 'admin', ], UserInfo::class);
// Expects object(GuestUserInfo) $result = $mapper->denormalize([ 'name' => 'Kirill', 'type' => 'guest', ], UserInfo::class);

The metadata configuration for this rule will look like this:

use TypeLang\Mapper\Mapping\DiscriminatorMap; #[DiscriminatorMap('type', [ 'admin' => AdminUserInfo::class, 'guest' => GuestUserInfo::class, ])] abstract class UserInfo { // ... }
// UserInfo.php return [ 'discriminator' => [ 'field' => 'type', 'map' => [ 'admin' => AdminUserInfo::class, 'guest' => GuestUserInfo::class, ], ], ];
// UserInfo.json { "discriminator": { "field": "type", "map": { "admin": "AdminUserInfo", "guest": "GuestUserInfo" } } }
# UserInfo.yaml discriminator: field: type map: admin: AdminUserInfo guest: GuestUserInfo
# UserInfo.neon discriminator: field: type map: admin: AdminUserInfo guest: GuestUserInfo

Default Type

When specifying such a configuration, another error may occur if the "type" field is not passed or an incorrect value is passed.

$result = (new \TypeLang\Mapper\Mapper()) ->denormalize([ 'name' => 'Kirill', ], UserInfo::class); // Object {"name": "Kirill"} requires missing field // "type" of type "admin"|"guest"
$result = (new \TypeLang\Mapper\Mapper()) ->denormalize([ 'name' => 'Kirill', 'type' => 'unknown', ], UserInfo::class); // Passed value in "type" of {"name": "Kirill", "type": "unknown"} // must be of type "admin"|"guest", but "unknown" given

To solve such problems, you can specify a default value (default type) that will be used in case of an incorrect or missing field in the discriminator map.

Let's define that if the map discriminator fails, then we will create a GuestUserInfo instance.

use TypeLang\Mapper\Mapping\DiscriminatorMap; #[DiscriminatorMap('type', [ 'admin' => AdminUserInfo::class, 'guest' => GuestUserInfo::class, ], otherwise: GuestUserInfo::class)] abstract class UserInfo { // ... }
// UserInfo.php return [ 'discriminator' => [ 'field' => 'type', 'map' => [ 'admin' => AdminUserInfo::class, 'guest' => GuestUserInfo::class, ], 'otherwise' => GuestUserInfo::class, ], ];
// UserInfo.json { "discriminator": { "field": "type", "map": { "admin": "AdminUserInfo", "guest": "GuestUserInfo" }, "otherwise": "GuestUserInfo" } }
# UserInfo.yaml discriminator: field: type map: admin: AdminUserInfo guest: GuestUserInfo otherwise: GuestUserInfo
# UserInfo.neon discriminator: field: type map: admin: AdminUserInfo guest: GuestUserInfo otherwise: GuestUserInfo

Properties

For each class, you can specify a list of properties and their types that will be involved in normalization and denormalization process.

For example, we have the following class:

final class UserInfo { public function __construct( public mixed $name, public mixed $other { get => $this->other; set(mixed $other) => (string) $other; }, ) {} }

To specify properties and their types, you can use the following settings

use TypeLang\Mapper\Mapping\MapType; final class UserInfo { public function __construct( #[MapType('string')] public mixed $name, #[MapType('string')] public mixed $other { get => $this->other; #[MapType('string|Stringable')] set(mixed $other) => (string) $other; }, ) {} }

As you can see, the #[MapType] attribute can be set on both properties and property hooks. Depending on where you specify the attribute, the corresponding rule will be applied.

#[MapType(...)] // Read + Write type public mixed $other { #[MapType(...)] // Read type get => $this->other; #[MapType(...)] // Write type set(mixed $other) => ...; },
final class UserInfo { public function __construct( /** * @var string */ public mixed $name, /** * @var string */ public mixed $other { get => $this->other; /** * @param string|Stringable $other */ set(mixed $other) => (string) $other; }, ) {} }

For promoted properties, you can also use parameter (@param) PHPDoc annotations.

final class UserInfo { /** * @param string $name * @param string $other */ public function __construct( public mixed $name, public mixed $other { get => $this->other; /** * @param string|Stringable $other */ set(mixed $other) => (string) $other; }, ) {} }

Besides this, as you can see, an annotations can be set on arguments, properties and property hooks. Depending on where you specify the annotation, the corresponding rule will be applied.

final class UserInfo { /** * This annotation is read first and assumes read + write * type "PARAM_TYPE" to promoted property $other * * @param PARAM_TYPE $other */ public function __construct( /** * Next, the annotation of the property is read and * overrides the read + write types by "PROPERTY_TYPE" * * This annotation has a higher priority than "param" * * @var PROPERTY_TYPE */ public mixed $other { /** * If the "return" annotation is specified on the * hook's "getter", the read type is directly the * type "GETTER_TYPE" * * The annotation replaces the READ type exclusively * * @return GETTER_TYPE */ get => $this->other; /** * If the "param" annotation is specified on the * hook's "setter", the write type is directly the * type "SETTER_TYPE" * * The annotation replaces the WRITE type exclusively * * @param SETTER_TYPE $other */ set(mixed $other) => (string) $other; }, ) {} }

To use reflection, simply specify the types. Please note that only public properties are read.

final class UserInfo { public function __construct( public string $name, public string $other { get => $this->other; set(string|Stringable $other) => (string) $other; }, private string $hiddenProperty, ) {} }
// UserInfo.php return [ 'properties' => [ 'name' => 'string', 'other' => [ 'type' => 'string', 'write' => 'string|Stringable', ], ], ];
// UserInfo.json { "properties": { "name": "string", "other": { "type": "string", "write": "string|Stringable" } } }
# UserInfo.yaml properties: name: string other: type: string write: string|Stringable
# UserInfo.neon properties: name: string other: type: string write: string|Stringable

Property Metadata

As you may have noticed above, a class can contain a collection of properties. However, for each property, you can also specify a list of additional rules. One such rule is the type, the configuration information for which is available above in "properties" section.

Let's look at other configuration rules.

Strict Types

For each property, you can also separately specify "type strictness" rules, more details about which can be found either in the configuration or in the section with type coercers.

The "strict types" option can take one of two possible values, or may not be specified:

  • In case of the true value specified, then "strict types" will be applied for the property

  • In case of the false value specified, then "strict types" will be disabled for the property

  • If you omit this option, the default value specified in the configuration will be used

Let's take the following class and apply different strictness rules to each property:

final class UserInfo { public function __construct( // Type should be strict public string $strict, // Type should be non-strict public string $notStrict, ) {} }
use TypeLang\Mapper\Mapping\MapType; final class UserInfo { public function __construct( #[MapType('string', strict: true)] public string $strict, #[MapType('string', strict: false)] public string $notStrict, ) {} }
// UserInfo.php return [ 'properties' => [ 'strict' => [ 'type' => 'string', 'strict' => true, ], 'notStrict' => [ 'type' => 'string', 'strict' => false, ], ], ];
// UserInfo.json { "properties": { "strict": { "type": "string", "strict": true }, "notStrict": { "type": "string", "strict": false } } }
# UserInfo.yaml properties: strict: type: string strict: true notStrict: type: string strict: false
# UserInfo.neon properties: strict: type: string strict: true notStrict: type: string strict: false

Rename (Alias)

You can specify the name of the property to be used during normalization. This way, when normalizing an object with property, it will be "published" under a different name.

use TypeLang\Mapper\Mapping\MapName; final class UserInfo { public function __construct( #[MapName('public_name')] public string $localName, ) {} }
// UserInfo.php return [ 'properties' => [ 'localName' => [ 'name' => 'public_name', ], ], ];
// UserInfo.json { "properties": { "localName": { "name": "public_name" } } }
# UserInfo.yaml properties: localName: name: public_name
# UserInfo.neon properties: localName: name: public_name

Custom Type Error Message

In some cases, you may need to override the error message when type errors occur. You can do this explicitly for a specific class property.

Please note that you also have access to template variables that substitute specific runtime values:

  • {{field}} - Property name (or public alias)

  • {{expected}} - Expected type

  • {{value}} - Actual value

  • {{path}} - The path to the property where the error occurred

  • {{file}} - PHP file in which the error occurred

  • {{line}} - The line in the PHP code where the error occurred

  • {{code}} - An error code

Let's specify an error message for property string $name, like:

The {{field}} is invalid

Thus, if you pass an empty array ([]) to "name" property, the following error should occur:

The "name" is invalid
use TypeLang\Mapper\Mapping\OnTypeError; final class UserInfo { public function __construct( #[OnTypeError('The {{field}} is invalid')] public string $name, ) {} }
// UserInfo.php return [ 'properties' => [ 'name' => [ 'type_error_message' => 'The {{field}} is invalid', ], ], ];
// UserInfo.json { "properties": { "name": { "type_error_message": "The {{field}} is invalid" } } }
# UserInfo.yaml properties: name: type_error_message: "The {{field}} is invalid"
# UserInfo.neon properties: name: type_error_message: "The {{field}} is invalid"

Custom Undefined Error Message

If a required field was not passed, a different error may occur. You can also customize it using the corresponding configuration rules.

use TypeLang\Mapper\Mapping\OnUndefinedError; final class UserInfo { public function __construct( #[OnUndefinedError('The {{field}} is required')] public string $name, ) {} }
// UserInfo.php return [ 'properties' => [ 'name' => [ 'undefined_error_message' => 'The {{field}} is required', ], ], ];
// UserInfo.json { "properties": { "name": { "undefined_error_message": "The {{field}} is required" } } }
# UserInfo.yaml properties: name: undefined_error_message: "The {{field}} is required"
# UserInfo.neon properties: name: undefined_error_message: "The {{field}} is required"

Skip

During normalization, there may be cases where a property needs to be excluded based on certain criteria. For this, you can use "skip" metadata configuration rules.

When Empty

If you want to exclude empty properties (for example, empty arrays), you can use the "when empty" rules

use TypeLang\Mapper\Mapping\SkipWhenEmpty; final class UserInfo { public function __construct( #[SkipWhenEmpty] public iterable $friends = [], ) {} }
// UserInfo.php return [ 'properties' => [ 'friends' => [ 'skip' => ['empty'], ], ], ];
// UserInfo.json { "properties": { "friends": { "skip": ["empty"] } } }
# UserInfo.yaml properties: friends: skip: - "empty"
# UserInfo.neon properties: friends: skip: - "empty"

When Null

If you want to exclude properties that are strictly null, you should use the "when null" setting.

use TypeLang\Mapper\Mapping\SkipWhenNull; final class UserInfo { public function __construct( #[SkipWhenNull] public ?string $name = null, ) {} }
// UserInfo.php return [ 'properties' => [ 'friends' => [ 'skip' => ['null'], ], ], ];
// UserInfo.json { "properties": { "friends": { "skip": ["null"] } } }
# UserInfo.yaml properties: friends: skip: - "null"
# UserInfo.neon properties: friends: skip: - "null"

Expression

In some cases, you may need to use more complex pass criteria. For these conditions, use the symfony/expression-language package.

use TypeLang\Mapper\Mapping\SkipWhen; final class UserInfoResponse { public function __construct( public ?string $firstName, public ?string $lastName, #[SkipWhen('this.firstName == null or this.lastName == null')] public string $fullName { get => $this->firstName . ' ' . $this->lastName; }, ) {} }
// UserInfo.php return [ 'properties' => [ 'fullName' => [ 'skip' => [ 'this.firstName == null or this.lastName == null' ], ], ], ];
// UserInfo.json { "properties": { "fullName": { "skip": [ "this.firstName == null or this.lastName == null" ] } } }
# UserInfo.yaml properties: friends: fullName: - "this.firstName == null or this.lastName == null"
# UserInfo.neon properties: friends: fullName: - "this.firstName == null or this.lastName == null"
06 November 2025