Tempest version
3.18
PHP version
8.5
Operating system
Linux
Description
When mapping an object to an array or JSON, ObjectToArrayMapper serializes
top-level scalar/date properties correctly, and it also serializes arrays of
objects correctly. However, a single nested object property is left as an object
instead of being recursively mapped to an array.
This becomes visible when the nested object contains a DateTime property:
top-level dates are serialized using the configured date serializer, but dates
inside the single nested object are later JSON-encoded as the internal public
structure of the DateTime object.
Current behavior
Given an object like this:
use Tempest\DateTime\DateTime;
use Tempest\DateTime\FormatPattern;
use Tempest\Validation\Rules\HasDateTimeFormat;
final class ChildObject
{
public function __construct(
#[HasDateTimeFormat(FormatPattern::ISO8601)]
public DateTime $createdAt,
) {}
}
final class ParentObject
{
public function __construct(
#[HasDateTimeFormat(FormatPattern::ISO8601)]
public DateTime $createdAt,
public ChildObject $child,
/** @var ChildObject[] */
public array $children,
) {}
}
And this mapper call:
$date = DateTime::parse('2026-08-19T12:34:56+00:00');
$array = map(new ParentObject(
createdAt: $date,
child: new ChildObject($date),
children: [new ChildObject($date)],
))->toArray();
The resulting array serializes the top-level date and the array-of-objects date,
but leaves the single nested child object unmapped:
[
'createdAt' => '2026-08-19T12:34:56.000Z',
'child' => ChildObject { ... },
'children' => [
['createdAt' => '2026-08-19T12:34:56.000Z'],
],
]
When using toJson(), the nested date is then encoded as the public structure of
the Tempest\DateTime\DateTime object, instead of as the configured date string:
{
"createdAt": "2026-08-19T12:34:56.000Z",
"child": {
"createdAt": {
"timezone": "UTC",
"timestamp": {
"seconds": 1787142896,
"nanoseconds": 0
},
"year": 2026,
"month": 8,
"day": 19,
"hours": 12,
"minutes": 34,
"seconds": 56,
"nanoseconds": 0
}
},
"children": [
{
"createdAt": "2026-08-19T12:34:56.000Z"
}
]
}
Expected behavior
Single nested object properties should be recursively mapped in the same way as
objects inside array properties.
Expected toArray() output:
[
'createdAt' => '2026-08-19T12:34:56.000Z',
'child' => [
'createdAt' => '2026-08-19T12:34:56.000Z',
],
'children' => [
['createdAt' => '2026-08-19T12:34:56.000Z'],
],
]
Expected toJson() output should contain the nested date as a string as well:
{
"createdAt": "2026-08-19T12:34:56.000Z",
"child": {
"createdAt": "2026-08-19T12:34:56.000Z"
},
"children": [
{
"createdAt": "2026-08-19T12:34:56.000Z"
}
]
}
Possible cause
ObjectToArrayMapper::resolvePropertyValue() currently handles iterable object
properties recursively:
if ($property->getIterableType()?->isClass()) {
foreach ($propertyValue as $key => $value) {
if (! is_object($value)) {
continue;
}
$propertyValue[$key] = map($value)->toArray();
}
return $propertyValue;
}
But single object properties do not seem to go through an equivalent recursive
object-to-array mapping step. If no serializer is selected for that property,
the object is returned as-is.
Suggested regression tests
This could be covered with tests in ObjectToArrayMapperTest and
ObjectToJsonMapperTest:
#[Test]
public function object_with_single_nested_object_to_array(): void
{
$date = DateTime::parse('2026-08-19T12:34:56+00:00');
$array = map(new ParentObject(
createdAt: $date,
child: new ChildObject($date),
children: [new ChildObject($date)],
))->toArray();
$this->assertSame(
[
'createdAt' => '2026-08-19T12:34:56.000Z',
'child' => [
'createdAt' => '2026-08-19T12:34:56.000Z',
],
'children' => [
['createdAt' => '2026-08-19T12:34:56.000Z'],
],
],
$array,
);
}
If this behavior is considered unintended, I can send a focused PR with regression tests for ObjectToArrayMapper and ObjectToJsonMapper.
Tempest version
3.18
PHP version
8.5
Operating system
Linux
Description
When mapping an object to an array or JSON,
ObjectToArrayMapperserializestop-level scalar/date properties correctly, and it also serializes arrays of
objects correctly. However, a single nested object property is left as an object
instead of being recursively mapped to an array.
This becomes visible when the nested object contains a
DateTimeproperty:top-level dates are serialized using the configured date serializer, but dates
inside the single nested object are later JSON-encoded as the internal public
structure of the
DateTimeobject.Current behavior
Given an object like this:
And this mapper call:
The resulting array serializes the top-level date and the array-of-objects date,
but leaves the single nested
childobject unmapped:[ 'createdAt' => '2026-08-19T12:34:56.000Z', 'child' => ChildObject { ... }, 'children' => [ ['createdAt' => '2026-08-19T12:34:56.000Z'], ], ]When using
toJson(), the nested date is then encoded as the public structure ofthe
Tempest\DateTime\DateTimeobject, instead of as the configured date string:{ "createdAt": "2026-08-19T12:34:56.000Z", "child": { "createdAt": { "timezone": "UTC", "timestamp": { "seconds": 1787142896, "nanoseconds": 0 }, "year": 2026, "month": 8, "day": 19, "hours": 12, "minutes": 34, "seconds": 56, "nanoseconds": 0 } }, "children": [ { "createdAt": "2026-08-19T12:34:56.000Z" } ] }Expected behavior
Single nested object properties should be recursively mapped in the same way as
objects inside array properties.
Expected
toArray()output:[ 'createdAt' => '2026-08-19T12:34:56.000Z', 'child' => [ 'createdAt' => '2026-08-19T12:34:56.000Z', ], 'children' => [ ['createdAt' => '2026-08-19T12:34:56.000Z'], ], ]Expected
toJson()output should contain the nested date as a string as well:{ "createdAt": "2026-08-19T12:34:56.000Z", "child": { "createdAt": "2026-08-19T12:34:56.000Z" }, "children": [ { "createdAt": "2026-08-19T12:34:56.000Z" } ] }Possible cause
ObjectToArrayMapper::resolvePropertyValue()currently handles iterable objectproperties recursively:
But single object properties do not seem to go through an equivalent recursive
object-to-array mapping step. If no serializer is selected for that property,
the object is returned as-is.
Suggested regression tests
This could be covered with tests in
ObjectToArrayMapperTestandObjectToJsonMapperTest:If this behavior is considered unintended, I can send a focused PR with regression tests for
ObjectToArrayMapperandObjectToJsonMapper.