Serialization
The serialization system converts PHP objects to JSON and back, driven entirely by PHP attributes on
properties. The same #[Json] attribute controls both directions: when serializing (object → JSON) it
determines which properties are included and how they are named; when deserializing (JSON → object) it maps
incoming keys back to the right properties and handles type coercion automatically.
Making a class serializable
Implement JsonSerializableInterface and use JsonSerializeTrait. The trait provides all the serialization
and deserialization methods; the interface enforces the contract.
Only properties annotated with #[Json] are included in serialization/deserialization. Properties without
the attribute are ignored in both directions.
Serialization
$model->toJson(); // → JSON string
$model->toJsonData(); // → PHP array ready for json_encode
$model->toArray(); // alias for toJsonData()
// Static helpers for collections
ProductModel::toJsonList($models); // → JSON array string
ProductModel::toJsonListData($models); // → array of PHP arrays
Deserialization
$model = ProductModel::fromJsonString($jsonString);
$model = ProductModel::fromJsonData($assocArray);
// Nested path: extract from $data['response']['product']
$model = ProductModel::fromJsonData($data, ['response', 'product']);
// Collections
$models = ProductModel::listFromJsonString($jsonString);
$models = ProductModel::listFromJsonData($assocArray, 'items');
#[Json] attribute reference
#[Json(
path: '', // string or string[] — JSON key(s). Defaults to the property name.
type: '', // class-string — element type for arrays or typed collections.
skipEmpty: false, // omit the key from output when value is null, '' or [].
factoryMethod: '', // method name used to construct a typed collection from an array.
required: false, // throw SerializationException if the key is absent during deserialization.
emptyArrayAsObject: false, // serialize an empty array as {} instead of [].
leaveUninitializedIfEmpty: false, // leave the property uninitialized (rather than null) when the key is absent.
jsonString: false, // the JSON value is itself a JSON-encoded string; decode it before processing.
)]
Custom key name
#[Json('price_incl_tax')]
public readonly float $price;
// serializes as {"price_incl_tax": 9.99}
// deserializes from {"price_incl_tax": 9.99}
Nested path
#[Json(['address', 'city'])]
public readonly string $city;
// deserializes from {"address": {"city": "Zagreb"}}
// serializes as {"address": {"city": "Zagreb"}}
Arrays of objects
Set type to the element class. Elements must also implement JsonSerializableInterface (or be
BackedEnum).
Nullable backed enums
#[Json]
public readonly ?StatusEnum $status;
// uses StatusEnum::tryFrom() when nullable, StatusEnum::from() otherwise
Collections (Traversable)
When the property type is a Traversable subclass, pass type for the element class and optionally
factoryMethod for construction:
If factoryMethod is a static method, it is called as TagCollection::fromArray($mapped). If it is an
instance method, an instance is created without constructor and the method is called on it.
Type coercion rules
During deserialization, the #[Json] attribute resolves the value type automatically:
| PHP type | Resolution |
|---|---|
Scalar (int, string, float, bool) |
Passed through as-is. |
Class implementing JsonSerializableInterface |
ClassName::fromJsonData($value) |
BackedEnum |
ClassName::from($value) (non-nullable) / ClassName::tryFrom($value) (nullable) |
Pure UnitEnum |
Serialized using ->name; not automatically deserialized. |
array with type set to a class |
array_map(fn($d) => Type::fromJsonData($d), $value) |
array with type set to a BackedEnum |
array_map(fn($d) => Type::from($d), $value) |
Traversable subclass with type set |
Elements mapped then passed to constructor / factoryMethod. |
During serialization, nested objects call toJsonData() on themselves if they implement
JsonDataWriteInterface, or use their ->name if they are a pure enum.
Serialization groups (#[Context])
Use #[Context] on a property to restrict it to specific normalization or denormalization groups. Properties
with no #[Context] are always included.
use Amarant\Framework\Attribute\Serialization\Context;
#[Json]
#[Context(groups: ['detail'], denormalizationGroups: ['write'])]
public readonly string $internalNote;
Pass group names when calling the Serializer service to filter properties:
$serializer->serialize($model, normalizationContext: ['detail']);
$serializer->deserialize($json, MyModel::class, denormalizationContext: ['write']);
Properties whose groups have no intersection with the active context are skipped entirely.
Polymorphic deserialization (#[Discriminator])
Use #[Discriminator] on a base class to select a concrete subclass during deserialization based on a
type-discriminator field in the JSON.
use Amarant\Framework\Attribute\Data\Discriminator;
#[Discriminator(
propertyName: 'type',
mapping: [
'physical' => PhysicalProductModel::class,
'digital' => DigitalProductModel::class,
]
)]
abstract class ProductModel implements JsonSerializableInterface
{
use JsonSerializeTrait;
}
When deserializing, the value of the type key is looked up in mapping and fromJsonData is delegated to
the matched subclass. Throws SerializationException if the discriminator value is absent from the mapping.
Using the Serializer service
Inject SerializerInterface when you need to serialize through the framework's event pipeline (which allows
subscribers to modify context and inject callbacks).
use Amarant\Framework\Contract\Serialization\SerializerInterface;
$json = $this->serializer->serialize($model);
$json = $this->serializer->serialize(
$model,
normalizationContext: ['detail'],
);
$model = $this->serializer->deserialize(
$jsonString,
ProductModel::class,
denormalizationContext: ['write'],
);
Calling toJson() / fromJsonData() directly on the object bypasses the event pipeline. Use the
Serializer service when event subscribers need to participate (e.g. to append extra fields or validate
input).
Events
Three events fire during serialization/deserialization, all using EventEnum constants:
| Event | EventEnum case |
Fired when |
|---|---|---|
| Serialize before | EventEnum::SERIALIZATION_SERIALIZE_BEFORE |
Before json_encode is called. Normalization context and extra context can be modified. |
| Serialize after | EventEnum::SERIALIZATION_SERIALIZE_AFTER |
After the JSON string is produced. Read-only access to the result. |
| Deserialize before | EventEnum::SERIALIZATION_DESERIALIZE_BEFORE |
Before fromJsonData is called. Raw input data and denormalization context can be modified. |
Adding per-request callbacks from a subscriber
SerializerSerializeBeforeEvent exposes convenience methods to attach inline callbacks that run during
toJsonData() on every object in the current serialization call:
use Amarant\Framework\Serialization\Event\SerializerSerializeBeforeEvent;
$event->addJsonSerializationBeforeCallback(function (object $subject): void {
// called before each object's properties are iterated
});
$event->addJsonSerializationAfterCallback(function (object $subject, array &$result): void {
// called after each object's toJsonData() returns; $result can be modified
$result['_extra'] = 'injected';
});
$event->addJsonSerializationNormalizeValueCallback(function (mixed $value): mixed {
// called for every nested object value that doesn't implement JsonDataWriteInterface
return $value;
});
SerializerDeserializeBeforeEvent provides the equivalent for deserialization: