Add #[JsonProperty] attribute and runtime improvements#8
Merged
thiagocordeiro merged 8 commits intomainfrom May 3, 2026
Merged
Add #[JsonProperty] attribute and runtime improvements#8thiagocordeiro merged 8 commits intomainfrom
thiagocordeiro merged 8 commits intomainfrom
Conversation
- RuntimeWriter: fix "valur" typo in exception message - RuntimeReader: rename shadowed $node in readValues catch block - RuntimeTypeNodeFactory: tighten $nodes cache to private - SerializerTestCase: remove unused initializeReadNodes/initializeNode helpers (referenced a node property that no longer exists on InputNode) - CI: drop deprecated --no-suggest flag from composer install - Bump PHPUnit to 12.5.24 to clear CVE-2026-24765 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- RuntimeReader::readList/readArrayMap: append item index to error path (e.g. .addresses.[2].place) so failures inside collections are pinpointable instead of being collapsed onto the parent path. - RuntimeWriter: same treatment when writing arrays — replace the generic '[]' segment with the concrete key. - RuntimeReader::readBool: switch from FILTER_VALIDATE_BOOL (which silently coerces unparseable input to false) to FILTER_NULL_ON_FAILURE and throw UnableToParseValue when the value cannot be coerced. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The $nodes cache was a private static array, which made it process-wide shared. That meant test cases leaked state between each other and two factories with different intended scopes silently saw each other's entries. Switch to a private instance array. Each factory now owns its own cache for its lifetime. Behavior for a single long-lived mapper is unchanged; fresh mappers (and tests) start with a clean slate as expected. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Previously a default ArrayObjectMapper instantiated three separate RuntimeTypeNodeFactory instances — one inside the default RuntimeReader, one inside the default RuntimeWriter, and one inside the default RuntimeTypeNodeSpecificationFactory. Each kept its own cache for the same types, so identical TypeNodes were rebuilt up to three times per mapper. Add an optional TypeNodeFactory parameter to ArrayObjectMapper (and JsonObjectMapper, which delegates to it). When the caller doesn't pass explicit reader/writer, we wire them up to share that single factory, collapsing the three caches into one. Callers that pass their own reader/writer keep full control as before. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Mirrors Jackson's @JsonProperty so users can pin a JSON key independently of the PHP identifier. Supports both read and write directions and propagates the wire name into error paths and the specification factory output. Implementation: - New attribute Tcds\Io\Jackson\Node\JsonProperty (TARGET_PARAMETER | TARGET_PROPERTY) so it works on promoted constructor params and on plain properties alike. - InputNode gains an optional ?string $key with a key() accessor that falls back to $name. RuntimeReader uses key() for both data lookup and error path; the named-arg key into `new $class(...$values)` stays as $name (constructor still expects PHP names). - OutputNode::property/method gain an optional $key; when set, $name becomes the wire key and $accessor stays as the PHP identifier used by read(). - RuntimeTypeNodeFactory::fromClass reads JsonProperty off both ReflectionMethodParameter and ReflectionProperty via a small jsonKey() helper and threads it into the node constructors. - RuntimeTypeNodeSpecificationFactory now keys the spec by the wire name so users see the JSON shape they will actually send. Tests: - New SnakeCaseDto fixture mixing two #[JsonProperty('snake_case')] fields with one untouched field. - JsonPropertyTest covers read (array + JSON), write (array + JSON), error path containing the wire key, and the specification output. Out of scope: shape-type keys (already explicit), multi-alias support, duplicate-wire-key validation. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
PHPStan level=max wants explicit T on Reader/Writer property declarations. The promoted-property version inherited these from the constructor docblock; the explicit declarations introduced in the shared-factory commit dropped them. Restore @var Reader<mixed> and Writer<mixed> on the class properties. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
When a required field is missing at the top level, readClass catches the constructor TypeError and rethrows UnableToParseValue with the *incoming* path (empty for the root) — same behavior as ErrorHandlingTest::missing_root_value. So the wire key only enters the path when the failure happens inside a nested class. Add SnakeCaseWrapper fixture (a class with one #[JsonProperty] field that itself holds a SnakeCaseDto) and assert the wire key appears in the path when the inner DTO fails. Also split out a separate test for $exception->expected so coverage of "spec uses wire keys" is exercised both at factory level and via the live exception. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
SnakeCaseWrapper had a single field, so readValues took the count===1 shortcut into readValueObject, which doesn't accumulate the wire key onto the path before recursing. The inner DTO failure therefore surfaced with path=[]. Add a second field on the wrapper so the main foreach in readValues runs and threads the wire key into innerpath as expected. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Stacks four related improvements toward a stable v1, ending in a Jackson-style
#[JsonProperty]attribute. Each commit is self-contained and reviewable on its own.readList/readArrayMapandRuntimeWriternow append the concrete index/key to the error path instead of a generic[].readBoolswitches toFILTER_NULL_ON_FAILUREand throwsUnableToParseValuefor unparseable input rather than silently coercing tofalse.RuntimeTypeNodeFactorycache to instance state — was process-wide static, now per-instance. Tests no longer leak through it.TypeNodeFactoryacross reader, writer and spec —ArrayObjectMapper/JsonObjectMapperaccept an optionalTypeNodeFactoryand wire defaults to share it, collapsing three duplicate caches into one.defaultTypeReader/defaultTypeWriterbecome nullable.#[JsonProperty]attribute — pin the JSON wire name independently of the PHP identifier on both read and write paths. Error traces and the specification factory output use the wire name. NewSnakeCaseDtofixture andJsonPropertyTestcover read, write, error path, and spec.Test plan
composer test:unit— existing suite stays green (no fixture usedkey, so defaults preserve current behavior)composer test:unit— newJsonPropertyTestpasses (read, write, error path, spec)composer test:stanat level=max stays clean{"created_at": "2025-01-01"}through a class with#[JsonProperty('created_at')] DateTimeImmutable $createdAt— confirms DateTime mapper composition still works🤖 Generated with Claude Code