From 400007c487b34e9c2893ced8b78c54cf9881ab82 Mon Sep 17 00:00:00 2001 From: Elisha Date: Fri, 29 May 2026 16:41:26 +0200 Subject: [PATCH] Skip validation when decoding API responses Component::__construct() ran Symfony validation on every construction, including while decoding API responses via decode(). Responses may legitimately omit fields that are only required when creating a pass (e.g. a partial classReference when updating a loyalty object), so decoding threw spurious validation errors. Skip validation during decode() (restored afterwards, so nested decoding is handled). Validation still runs for objects constructed directly to be sent. Fixes chiiya/laravel-passes#40 --- src/Common/Component.php | 23 ++++++++++++++++ tests/Common/ComponentTest.php | 50 ++++++++++++++++++++++++++++++++++ 2 files changed, 73 insertions(+) create mode 100644 tests/Common/ComponentTest.php diff --git a/src/Common/Component.php b/src/Common/Component.php index 4ece05c..6e58e46 100644 --- a/src/Common/Component.php +++ b/src/Common/Component.php @@ -9,8 +9,19 @@ abstract class Component extends DataTransferObject implements JsonSerializable { + /** + * Whether constructed components should be validated. Disabled while decoding API + * responses, since responses may legitimately omit fields that are only required + * when creating a pass. Validation still runs for objects constructed directly. + */ + private static bool $validate = true; + public function __construct() { + if (! self::$validate) { + return; + } + $validator = Validation::createValidatorBuilder()->enableAttributeMapping()->getValidator(); $errors = $validator->validate($this); @@ -25,6 +36,18 @@ public function __construct() } } + public static function decode(array $data): static + { + $previous = self::$validate; + self::$validate = false; + + try { + return parent::decode($data); + } finally { + self::$validate = $previous; + } + } + /** * Convert the object into something JSON serializable. */ diff --git a/tests/Common/ComponentTest.php b/tests/Common/ComponentTest.php new file mode 100644 index 0000000..9110619 --- /dev/null +++ b/tests/Common/ComponentTest.php @@ -0,0 +1,50 @@ + 'SOME_FUTURE_TYPE']); + + $this->assertSame('SOME_FUTURE_TYPE', $message->messageType); + } + + #[Group('common')] + public function test_direct_construction_still_runs_validation(): void + { + $this->expectException(ValidationException::class); + + new Message(messageType: 'SOME_FUTURE_TYPE'); + } + + #[Group('google')] + public function test_decode_skips_validation_for_nested_components(): void + { + // Regression for chiiya/laravel-passes#40: updating a loyalty pass decodes the + // response, whose classReference may carry values/fields our validation rejects. + $object = LoyaltyObject::decode([ + 'id' => 'issuer.object', + 'classId' => 'issuer.class', + 'state' => 'ACTIVE', + 'classReference' => [ + 'id' => 'issuer.class', + 'reviewStatus' => 'SOME_FUTURE_STATUS', + 'programLogo' => ['sourceUri' => ['uri' => 'https://example.com/logo.png']], + ], + ]); + + $this->assertSame('SOME_FUTURE_STATUS', $object->classReference->reviewStatus); + $this->assertNull($object->classReference->issuerName); + } +}