diff --git a/app/SchemaValidator.php b/app/SchemaValidator.php index 6511dbe..50fbfd6 100644 --- a/app/SchemaValidator.php +++ b/app/SchemaValidator.php @@ -16,8 +16,9 @@ * @method static array|object getSchemaContents(string $relativeUri, bool $associative = true) * @method static void putSchemaContents(string $relativeUri, array|object $schema) * @method static string registerRawSchema(bool|object|string $schema) - * @method static bool validate(array|object $data, string $schema) - * @method static bool validateOrThrow(string|array|object $data, string $schema, string $exceptionMessage = null, bool $appendValidationDescriptions = false, int $failureHttpStatusCode = Response::HTTP_BAD_REQUEST) + * @method static bool validate(mixed $data, string $schema) + * @method static bool validateOrThrow(mixed $data, string $schema, string $exceptionMessage = null, bool $appendValidationDescriptions = false, int $failureHttpStatusCode = Response::HTTP_BAD_REQUEST) + * @method static bool validateEncodedStringOrThrow(mixed $data, string $schema, string $exceptionMessage = null, bool $appendValidationDescriptions = false, int $failureHttpStatusCode = Response::HTTP_BAD_REQUEST) */ class SchemaValidator extends Facade { diff --git a/app/SchemaValidatorService.php b/app/SchemaValidatorService.php index 678d0ea..e985b6a 100644 --- a/app/SchemaValidatorService.php +++ b/app/SchemaValidatorService.php @@ -138,6 +138,24 @@ public function validateOrThrow( return true; } + /** + * This method will attempt to validate the provided JSON-encoded string against the provided schema. + */ + public function validateEncodedStringOrThrow( + string $data, + $schema, + string $exceptionMessage = null, + bool $appendValidationDescriptions = false, + int $failureHttpStatusCode = Response::HTTP_BAD_REQUEST, + ): bool + { + // Using the non-associative decode is both how Opis documents it + // https://opis.io/json-schema/2.x/quick-start.html + // and is known to avoid `{}` vs `[]` confusion + $decodedData = json_decode($data, associative: false, flags: JSON_THROW_ON_ERROR); + return $this->validateOrThrow($decodedData, $schema, $exceptionMessage, $appendValidationDescriptions, $failureHttpStatusCode); + } + /** * Given anything that Opis can use as a schema (object, boolean, json-encoded string) * register the schema into our namespace, so it can safely contain relative links of its own. diff --git a/tests/Feature/SchemaValidatorServiceTest.php b/tests/Feature/SchemaValidatorServiceTest.php index 945ed4d..e0a18a0 100644 --- a/tests/Feature/SchemaValidatorServiceTest.php +++ b/tests/Feature/SchemaValidatorServiceTest.php @@ -6,6 +6,7 @@ namespace Tests\Feature; +use Carsdotcom\JsonSchemaValidation\Exceptions\JsonSchemaValidationException; use Carsdotcom\JsonSchemaValidation\SchemaValidatorService; use Illuminate\Support\Facades\Config; use Opis\JsonSchema\Exceptions\UnresolvedReferenceException; @@ -87,4 +88,38 @@ public function testRegisterRawSchema(): void self::assertStringStartsWith(Config::get('json-schema.base_url'), $absoluteRaw); self::assertTrue($validator->validate($vins, $absoluteRaw)); } + + /** + * @dataProvider provideValidateEncodedStringOrThrow + */ + public function testValidateEncodedStringOrThrow(string $encodedData, mixed $schema, bool $expectedSuccess): void + { + $validator = new SchemaValidatorService(); + + try { + self::assertTrue($validator->validateEncodedStringOrThrow($encodedData, $schema)); + if (!$expectedSuccess) { + self::fail("Should have thrown JsonSchemaValidationException"); + } + } catch (JsonSchemaValidationException $e) { + if ($expectedSuccess) { + self::assertTrue(false, "Expected success, instead got " . $e->errorsAsMultilineString()); + } else { + self::addToAssertionCount(1); + } + } + } + + public function provideValidateEncodedStringOrThrow(): array + { + return [ + 'primitive, string schema' => ['420', '{"type": "number", "minimum": 69}', true], + 'primitive, string schema fails' => ['42', '{"type": "number", "minimum": 69}', false], + 'empty object is still an object' => ['{}', '{"type": "object"}', true], + 'empty object is not an array' => ['{}', '{"type": "array"}', false], + 'typical complex object, success' => ['{"a":1}', '{"type":"object","properties":{"a":{"type":"number"}}, "required":["a"]}', true], + 'typical complex object, failure' => ['{"b":1}', '{"type":"object","properties":{"a":{"type":"number"}}, "required":["a"]}', false], + ]; + } + }