Repository: hotmeteor/spectator (php)

Issue:
Responses using application/problem+json content type cannot be decoded
I currently have several endpoints that can return possible errors/non-200 status codes. When these errors occur, the response `Content-Type` header is set to `application/problem+json` per [RFC 7807](https://datatracker.ietf.org/doc/html/rfc7807).

Attempting to validate one of these responses causes this error to be thrown

```json
[{
    "exception": "Spectator\\Exceptions\\ResponseValidationException",
    "message": "Unable to map [application/problem+json] to schema type [object].",
    "specErrors": []
}]
```

I tracked this down to the `body` method of `ResponseValidator`

```php
   protected function body($contentType, $schemaType)
    {
        $body = $this->response->getContent();

        if (in_array($schemaType, ['object', 'array', 'allOf', 'anyOf', 'oneOf'], true)) {
            if (in_array($contentType, ['application/json', 'application/vnd.api+json'])) {
                return json_decode($body);
            } else {
                throw new ResponseValidationException("Unable to map [{$contentType}] to schema type [object].");
            }
        }

        return $body;
    }
```

From my perspective, `application/problem+json` should be added to the content type check and decoded as normal.

Pull request:
Add support to decode responses using application/problem+json content type
Fixes #103.

Patch:
diff --git a/src/Validation/ResponseValidator.php b/src/Validation/ResponseValidator.php
index 02cc906..27f3562 100644
--- a/src/Validation/ResponseValidator.php
+++ b/src/Validation/ResponseValidator.php
@@ -150,7 +150,7 @@ class ResponseValidator extends AbstractValidator
         $body = $this->response->getContent();
 
         if (in_array($schemaType, ['object', 'array', 'allOf', 'anyOf', 'oneOf'], true)) {
-            if (in_array($contentType, ['application/json', 'application/vnd.api+json'])) {
+            if (in_array($contentType, ['application/json', 'application/vnd.api+json', 'application/problem+json'])) {
                 return json_decode($body);
             } else {
                 throw new ResponseValidationException("Unable to map [{$contentType}] to schema type [object].");


Test patch:
diff --git a/tests/Fixtures/Test.v1.json b/tests/Fixtures/Test.v1.json
index 1d8db01..f06e5e7 100644
--- a/tests/Fixtures/Test.v1.json
+++ b/tests/Fixtures/Test.v1.json
@@ -56,6 +56,48 @@
                 }
               }
             }
+          },
+          "422": {
+            "description": "Unprocessable entity",
+            "content": {
+              "application/problem+json": {
+                "schema": {
+                  "type": "array",
+                  "items": {
+                    "type": "object",
+                    "properties": {
+                      "id": {
+                        "type": "number",
+                        "description": "User ID",
+                        "example": 1
+                      },
+                      "name": {
+                        "type": "string",
+                        "description": "User name",
+                        "example": "Adam Campbell"
+                      },
+                      "email": {
+                        "type": "string",
+                        "description": "User email address",
+                        "format": "email",
+                        "example": "test@test.com"
+                      }
+                    }
+                  }
+                },
+                "examples": {
+                  "example-1": {
+                    "value": [
+                      {
+                        "id": 1,
+                        "name": "Adam Campbell",
+                        "email": "test@test.com"
+                      }
+                    ]
+                  }
+                }
+              }
+            }
           }
         },
         "operationId": "get-users",
diff --git a/tests/Fixtures/Test.v1.yaml b/tests/Fixtures/Test.v1.yaml
index 3d4ebea..c1cb9eb 100644
--- a/tests/Fixtures/Test.v1.yaml
+++ b/tests/Fixtures/Test.v1.yaml
@@ -23,6 +23,19 @@ paths:
                     type: string
                   email:
                     type: string
+        '422':
+          description: Unprocessable entity
+          content:
+            application/problem+json:
+              schema:
+                type: object
+                properties:
+                  id:
+                    type: integer
+                  name:
+                    type: string
+                  email:
+                    type: string
       operationId: get-users
       parameters:
         - schema:
diff --git a/tests/ResponseValidatorTest.php b/tests/ResponseValidatorTest.php
index ff18861..13c346c 100644
--- a/tests/ResponseValidatorTest.php
+++ b/tests/ResponseValidatorTest.php
@@ -68,6 +68,53 @@ class ResponseValidatorTest extends TestCase
             ->assertValidationMessage('All array items must match schema');
     }
 
+    public function test_validates_valid_problem_json_response()
+    {
+        Route::get('/users', function () {
+            return response()->json([
+                [
+                    'id' => 1,
+                    'name' => 'Jim',
+                    'email' => 'test@test.test',
+                ],
+            ], 422, ['Content-Type' => 'application/problem+json']);
+        })->middleware(Middleware::class);
+
+        $this->getJson('/users')
+            ->assertValidRequest()
+            ->assertValidResponse(422);
+    }
+
+    public function test_validates_invalid_problem_json_response()
+    {
+        Route::get('/users', function () {
+            return response()->json([
+                [
+                    'id' => 'invalid',
+                ],
+            ], 422, ['Content-Type' => 'application/problem+json']);
+        })->middleware(Middleware::class);
+
+        $this->getJson('/users')
+            ->assertValidRequest()
+            ->assertInvalidResponse(400)
+            ->assertValidationMessage('All array items must match schema');
+
+        Route::get('/users', function () {
+            return response()->json([
+                [
+                    'id' => 1,
+                    'email' => 'invalid',
+                ],
+            ], 422, ['Content-Type' => 'application/problem+json']);
+        })->middleware(Middleware::class);
+
+        $this->getJson('/users')
+            ->assertValidRequest()
+            ->assertInvalidResponse(400)
+            ->assertValidationMessage('All array items must match schema');
+    }
+
     public function test_fallback_to_request_uri_if_operationId_not_given(): void
     {
         Spectator::using('Test.v1.json');