Repository: api-platform/core (php)

Issue:
APIP4, Hydra prefix , error response
**API Platform version(s) affected**: 4.1

**Description**  

API4 remove the hydra prefix, and we really need to be BC complient.
So I added
```yaml
api_platform:
    serializer:
        hydra_prefix: true
```

In my symfony application configuration

The GET endpoints looks fine, but when something goes wrong, the response has duplicated content. For exemple, when I faces a 422:
```json
{
  "@context": "\/app_dev.php\/api\/contexts\/ConstraintViolation",
  "@id": "\/app_dev.php\/api\/validation_errors\/c1051bb4-d103-4f74-8988-acbcafc7fdc3",
  "@type": "ConstraintViolation",
  "status": 422,
  "violations": [
    {
      "propertyPath": "project",
      "message": "This value should not be blank.",
      "code": "c1051bb4-d103-4f74-8988-acbcafc7fdc3"
    }
  ],
  "detail": "project: This value should not be blank.",
  "description": "project: This value should not be blank.",
  "type": "\/validation_errors\/c1051bb4-d103-4f74-8988-acbcafc7fdc3",
  "title": "An error occurred",
  "hydra:description": "project: This value should not be blank.",
  "hydra:title": "An error occurred"
}
```

See 
* `detail` vs `description` vs `hydra:description`
* `title` vs `hydra:title`



Pull request:
fix(jsonld): duplicate error fields when prefix is enabled
fixes #7000

Patch:
diff --git a/features/hydra/collection.feature b/features/hydra/collection.feature
index 06e8bda89..8e9b0c33a 100644
--- a/features/hydra/collection.feature
+++ b/features/hydra/collection.feature
@@ -479,7 +479,7 @@ Feature: Collections support
 
     When I send a "GET" request to "/dummies?itemsPerPage=0&page=2"
     Then the response status code should be 400
-    And the JSON node "description" should be equal to "Page should not be greater than 1 if limit is equal to 0"
+    And the JSON node "detail" should be equal to "Page should not be greater than 1 if limit is equal to 0"
 
   Scenario: Cursor-based pagination with an empty collection
     When I send a "GET" request to "/so_manies"
diff --git a/features/hydra/error.feature b/features/hydra/error.feature
index 4e336205c..07fe8210f 100644
--- a/features/hydra/error.feature
+++ b/features/hydra/error.feature
@@ -16,10 +16,10 @@ Feature: Error handling
     And the header "Content-Type" should be equal to "application/problem+json; charset=utf-8"
     And the header "Link" should contain '<http://www.w3.org/ns/hydra/error>; rel="http://www.w3.org/ns/json-ld#error"'
     And the JSON node "type" should exist
-    And the JSON node "title" should be equal to "An error occurred"
+    And the JSON node "title" should not exists
     And the JSON node "hydra:title" should be equal to "An error occurred"
     And the JSON node "detail" should exist
-    And the JSON node "description" should exist
+    And the JSON node "description" should not exist
     And the JSON node "hydra:description" should exist
     And the JSON node "trace" should exist
     And the JSON node "status" should exist
@@ -49,10 +49,8 @@ Feature: Error handling
               }
           ],
           "detail": "name: This value should not be blank.",
-          "title": "An error occurred",
           "hydra:title": "An error occurred",
           "hydra:description": "name: This value should not be blank.",
-          "description": "name: This value should not be blank.",
           "type": "/validation_errors/c1051bb4-d103-4f74-8988-acbcafc7fdc3"
       }
     """
@@ -69,7 +67,7 @@ Feature: Error handling
     And the header "Link" should contain '<http://www.w3.org/ns/hydra/error>; rel="http://www.w3.org/ns/json-ld#error"'
     And the JSON node "@context" should exist
     And the JSON node "type" should exist
-    And the JSON node "title" should be equal to "An error occurred"
+    And the JSON node "hydra:title" should be equal to "An error occurred"
     And the JSON node "detail" should exist
 
   Scenario: Get an rfc 7807 not found error
@@ -84,9 +82,9 @@ Feature: Error handling
     And the header "Link" should contain '<http://www.w3.org/ns/hydra/error>; rel="http://www.w3.org/ns/json-ld#error"'
     And the JSON node "@context" should exist
     And the JSON node "type" should exist
-    And the JSON node "title" should be equal to "An error occurred"
+    And the JSON node "hydra:title" should be equal to "An error occurred"
     And the JSON node "detail" should exist
-    And the JSON node "description" should exist
+    And the JSON node "description" should not exist
 
   Scenario: Get an rfc 7807 bad method error
     When I add "Content-Type" header equal to "application/ld+json"
@@ -101,9 +99,9 @@ Feature: Error handling
     And the header "Link" should contain '<http://www.w3.org/ns/hydra/error>; rel="http://www.w3.org/ns/json-ld#error"'
     And the JSON node "@context" should exist
     And the JSON node "type" should exist
-    And the JSON node "title" should be equal to "An error occurred"
+    And the JSON node "hydra:title" should be equal to "An error occurred"
     And the JSON node "detail" should exist
-    And the JSON node "description" should exist
+    And the JSON node "description" should not exist
 
   Scenario: Get an rfc 7807 validation error
     When I add "Content-Type" header equal to "application/ld+json"
@@ -118,7 +116,7 @@ Feature: Error handling
     And the header "Link" should contain '<http://www.w3.org/ns/hydra/error>; rel="http://www.w3.org/ns/json-ld#error"'
     And the JSON node "@context" should exist
     And the JSON node "type" should exist
-    And the JSON node "title" should be equal to "An error occurred"
+    And the JSON node "hydra:title" should be equal to "An error occurred"
     And the JSON node "detail" should exist
     And the JSON node "violations" should exist
 
@@ -133,8 +131,8 @@ Feature: Error handling
     And the header "Content-Type" should be equal to "application/problem+json; charset=utf-8"
     And the header "Link" should contain '<http://www.w3.org/ns/hydra/error>; rel="http://www.w3.org/ns/json-ld#error"'
     And the JSON node "type" should exist
-    And the JSON node "title" should be equal to "An error occurred"
+    And the JSON node "hydra:title" should be equal to "An error occurred"
     And the JSON node "detail" should exist
-    And the JSON node "description" should exist
+    And the JSON node "description" should not exist
     And the JSON node "trace" should exist
     And the JSON node "status" should exist
diff --git a/features/jsonld/input_output.feature b/features/jsonld/input_output.feature
index 6f65fa71c..3840877de 100644
--- a/features/jsonld/input_output.feature
+++ b/features/jsonld/input_output.feature
@@ -309,7 +309,7 @@ Feature: JSON-LD DTO input and output
     """
     Then the response status code should be 400
     And the response should be in JSON
-    And the JSON node "description" should be equal to "The input data is misformatted."
+    And the JSON node "detail" should be equal to "The input data is misformatted."
 
   @!mongodb
   Scenario: Reset password through an input DTO without DataTransformer
diff --git a/features/main/attribute_resource.feature b/features/main/attribute_resource.feature
index 016e3e999..da92073e9 100644
--- a/features/main/attribute_resource.feature
+++ b/features/main/attribute_resource.feature
@@ -100,7 +100,7 @@ Feature: Resource attributes
     And the response should be in JSON
     And the header "Content-Type" should be equal to "application/problem+json; charset=utf-8"
     And the header "Link" should contain '<http://www.w3.org/ns/hydra/error>; rel="http://www.w3.org/ns/json-ld#error"'
-    And the JSON node "description" should be equal to 'Unable to generate an IRI for the item of type "ApiPlatform\Tests\Fixtures\TestBundle\Entity\IncompleteUriVariableConfigured"'
+    And the JSON node "detail" should be equal to 'Unable to generate an IRI for the item of type "ApiPlatform\Tests\Fixtures\TestBundle\Entity\IncompleteUriVariableConfigured"'
 
   Scenario: Uri variables with Post operation
     When I add "Content-Type" header equal to "application/ld+json"
diff --git a/features/main/crud.feature b/features/main/crud.feature
index 7b6434f09..5933812bc 100644
--- a/features/main/crud.feature
+++ b/features/main/crud.feature
@@ -96,7 +96,7 @@ Feature: Create-Retrieve-Update-Delete
     When I add "Content-Type" header equal to "application/ld+json"
     And I send a "POST" request to "/dummies"
     Then the response status code should be 400
-    And the JSON node "description" should be equal to "Syntax error"
+    And the JSON node "detail" should be equal to "Syntax error"
 
   Scenario: Get a not found exception
     When I send a "GET" request to "/dummies/42"
@@ -554,7 +554,7 @@ Feature: Create-Retrieve-Update-Delete
     When I add "Content-Type" header equal to "application/ld+json"
     And I send a "PUT" request to "/dummies/1"
     Then the response status code should be 400
-    And the JSON node "description" should be equal to "Syntax error"
+    And the JSON node "detail" should be equal to "Syntax error"
 
   Scenario: Delete a resource
     When I send a "DELETE" request to "/dummies/1"
diff --git a/features/main/not_exposed.feature b/features/main/not_exposed.feature
index 5e6173052..809b95dd8 100644
--- a/features/main/not_exposed.feature
+++ b/features/main/not_exposed.feature
@@ -171,7 +171,7 @@ Feature: Expose only a collection of objects
     When I send a "GET" request to "<uri>"
     Then the response status code should be 404
     And the response should be in JSON
-    And the JSON node "description" should be equal to "<description>"
+    And the JSON node "detail" should be equal to "<description>"
     Examples:
       | uri                      | description                                                                                                          |
       | /tables/12345            | This route does not aim to be called.                                                                                      |
@@ -181,7 +181,7 @@ Feature: Expose only a collection of objects
     When I send a "GET" request to "<uri>"
     Then the response status code should be 404
     And the response should be in JSON
-    And the JSON node "description" should be equal to "<description>"
+    And the JSON node "detail" should be equal to "<description>"
     Examples:
       | uri                      | description                                                                                                          |
       | /.well-known/genid/12345 | This route is not exposed on purpose. It generates an IRI for a collection resource without identifier nor item operation. |
diff --git a/features/main/relation.feature b/features/main/relation.feature
index 0f16560df..5eba540f9 100644
--- a/features/main/relation.feature
+++ b/features/main/relation.feature
@@ -472,7 +472,7 @@ Feature: Relations support
     Then the response status code should be 400
     And the response should be in JSON
     And the header "Content-Type" should be equal to "application/problem+json; charset=utf-8"
-    And the JSON node "description" should contain 'Invalid IRI "certainly not an IRI".'
+    And the JSON node "detail" should contain 'Invalid IRI "certainly not an IRI".'
 
   Scenario: Passing an invalid type to a relation
     When I add "Content-Type" header equal to "application/ld+json"
@@ -495,18 +495,18 @@ Feature: Relations support
           "type": "string",
           "pattern": "^hydra:Error$"
         },
-        "title": {
+        "hydra:title": {
           "type": "string",
           "pattern": "^An error occurred$"
         },
-        "description": {
+        "detail": {
           "pattern": "^The type of the \"ApiPlatform\\\\Tests\\\\Fixtures\\\\TestBundle\\\\(Document|Entity)\\\\RelatedDummy\" resource must be \"array\" \\(nested document\\) or \"string\" \\(IRI\\), \"integer\" given.$"
         }
       },
       "required": [
         "@type",
-        "title",
-        "description"
+        "hydra:title",
+        "detail"
       ]
     }
     """
diff --git a/features/main/union_intersect_types.feature b/features/main/union_intersect_types.feature
index d11edd420..73195804d 100644
--- a/features/main/union_intersect_types.feature
+++ b/features/main/union_intersect_types.feature
@@ -118,4 +118,4 @@ Feature: Union/Intersect types
     Then the response status code should be 400
     And the response should be in JSON
     And the header "Content-Type" should be equal to "application/problem+json; charset=utf-8"
-    And the JSON node "description" should be equal to 'Could not denormalize object of type "ApiPlatform\Tests\Fixtures\TestBundle\ApiResource\Issue5452\ActivableInterface", no supporting normalizer found.'
+    And the JSON node "detail" should be equal to 'Could not denormalize object of type "ApiPlatform\Tests\Fixtures\TestBundle\ApiResource\Issue5452\ActivableInterface", no supporting normalizer found.'
diff --git a/features/main/validation.feature b/features/main/validation.feature
index dcfd05d34..82d93f939 100644
--- a/features/main/validation.feature
+++ b/features/main/validation.feature
@@ -31,8 +31,7 @@ Feature: Using validations groups
     {
       "@context": "/contexts/ConstraintViolation",
       "@type": "ConstraintViolation",
-      "title": "An error occurred",
-      "description": "name: This value should not be null.",
+      "detail": "name: This value should not be null.",
       "violations": [
          {
              "propertyPath": "name",
@@ -60,8 +59,7 @@ Feature: Using validations groups
     {
       "@context": "/contexts/ConstraintViolation",
       "@type": "ConstraintViolation",
-      "title": "An error occurred",
-      "description": "title: This value should not be null.",
+      "detail": "title: This value should not be null.",
       "violations": [
          {
              "propertyPath": "title",
@@ -87,7 +85,6 @@ Feature: Using validations groups
     And the JSON node "violations[0].message" should be equal to "This value should not be null."
     And the JSON node "violations[0].propertyPath" should be equal to "test"
     And the JSON node "detail" should be equal to "test: This value should not be null."
-    And the JSON node "description" should be equal to "test: This value should not be null."
     And the header "Content-Type" should be equal to "application/problem+json; charset=utf-8"
 
   @!mongodb
diff --git a/features/mongodb/filters.feature b/features/mongodb/filters.feature
index 7658ed5c3..5bb7c3b07 100644
--- a/features/mongodb/filters.feature
+++ b/features/mongodb/filters.feature
@@ -13,8 +13,7 @@ Feature: Filters on collections
     And the header "Content-Type" should be equal to "application/problem+json; charset=utf-8"
     And the JSON node "@context" should be equal to "/contexts/Error"
     And the JSON node "@type" should be equal to "hydra:Error"
-    And the JSON node "title" should be equal to "An error occurred"
-    And the JSON node "description" should be equal to "Cannot use reference 'badFourthLevel' in class 'ThirdLevel' for lookup or graphLookup: dbRef references are not supported."
+    And the JSON node "detail" should be equal to "Cannot use reference 'badFourthLevel' in class 'ThirdLevel' for lookup or graphLookup: dbRef references are not supported."
     And the JSON node "trace" should exist
 
   Scenario: Error when getting collection with nested properties if references are not correctly stored (not owning side)
@@ -24,6 +23,5 @@ Feature: Filters on collections
     And the header "Content-Type" should be equal to "application/problem+json; charset=utf-8"
     And the JSON node "@context" should be equal to "/contexts/Error"
     And the JSON node "@type" should be equal to "hydra:Error"
-    And the JSON node "title" should be equal to "An error occurred"
-    And the JSON node "description" should be equal to "Cannot use reference 'badThirdLevel' in class 'FourthLevel' for lookup or graphLookup: dbRef references are not supported."
+    And the JSON node "detail" should be equal to "Cannot use reference 'badThirdLevel' in class 'FourthLevel' for lookup or graphLookup: dbRef references are not supported."
     And the JSON node "trace" should exist
diff --git a/features/security/strong_typing.feature b/features/security/strong_typing.feature
index 9e78c9e89..27e669816 100644
--- a/features/security/strong_typing.feature
+++ b/features/security/strong_typing.feature
@@ -55,8 +55,7 @@ Feature: Handle properly invalid data submitted to the API
     And the header "Content-Type" should be equal to "application/problem+json; charset=utf-8"
     And the JSON node "@context" should be equal to "/contexts/Error"
     And the JSON node "@type" should be equal to "hydra:Error"
-    And the JSON node "title" should be equal to "An error occurred"
-    And the JSON node "description" should be equal to 'The type of the "name" attribute must be "string", "NULL" given.'
+    And the JSON node "detail" should be equal to 'The type of the "name" attribute must be "string", "NULL" given.'
 
   Scenario: Create a resource with wrong value type for relation
     When I add "Content-Type" header equal to "application/ld+json"
@@ -72,8 +71,7 @@ Feature: Handle properly invalid data submitted to the API
     And the header "Content-Type" should be equal to "application/problem+json; charset=utf-8"
     And the JSON node "@context" should be equal to "/contexts/Error"
     And the JSON node "@type" should be equal to "hydra:Error"
-    And the JSON node "title" should be equal to "An error occurred"
-    And the JSON node "description" should be equal to 'Invalid IRI "1".'
+    And the JSON node "detail" should be equal to 'Invalid IRI "1".'
     And the JSON node "trace" should exist
 
   Scenario: Ignore invalid dates
@@ -116,8 +114,7 @@ Feature: Handle properly invalid data submitted to the API
     And the header "Content-Type" should be equal to "application/problem+json; charset=utf-8"
     And the JSON node "@context" should be equal to "/contexts/Error"
     And the JSON node "@type" should be equal to "hydra:Error"
-    And the JSON node "title" should be equal to "An error occurred"
-    And the JSON node "description" should be equal to 'The type of the "relatedDummies" attribute must be "array", "string" given.'
+    And the JSON node "detail" should be equal to 'The type of the "relatedDummies" attribute must be "array", "string" given.'
     And the JSON node "trace" should exist
 
   Scenario: Send an object where an array is expected
@@ -134,8 +131,7 @@ Feature: Handle properly invalid data submitted to the API
     And the header "Content-Type" should be equal to "application/problem+json; charset=utf-8"
     And the JSON node "@context" should be equal to "/contexts/Error"
     And the JSON node "@type" should be equal to "hydra:Error"
-    And the JSON node "title" should be equal to "An error occurred"
-    And the JSON node "description" should be equal to 'The type of the key "a" must be "int", "string" given.'
+    And the JSON node "detail" should be equal to 'The type of the key "a" must be "int", "string" given.'
 
   Scenario: Send a scalar having the bad type
     When I add "Content-Type" header equal to "application/ld+json"
@@ -150,8 +146,7 @@ Feature: Handle properly invalid data submitted to the API
     And the header "Content-Type" should be equal to "application/problem+json; charset=utf-8"
     And the JSON node "@context" should be equal to "/contexts/Error"
     And the JSON node "@type" should be equal to "hydra:Error"
-    And the JSON node "title" should be equal to "An error occurred"
-    And the JSON node "description" should be equal to 'The type of the "name" attribute must be "string", "integer" given.'
+    And the JSON node "detail" should be equal to 'The type of the "name" attribute must be "string", "integer" given.'
 
   Scenario: According to the JSON spec, allow numbers without explicit floating point for JSON formats
     When I add "Content-Type" header equal to "application/ld+json"
diff --git a/features/security/validate_incoming_content-types.feature b/features/security/validate_incoming_content-types.feature
index 27a750008..80c6fd3b6 100644
--- a/features/security/validate_incoming_content-types.feature
+++ b/features/security/validate_incoming_content-types.feature
@@ -14,4 +14,4 @@ Feature: Validate incoming content type
     """
     Then the response status code should be 415
     And the header "Content-Type" should be equal to "application/problem+json; charset=utf-8"
-    And the JSON node "description" should be equal to 'The content-type "text/plain" is not supported. Supported MIME types are "application/ld+json", "application/hal+json", "application/vnd.api+json", "application/xml", "text/xml", "application/json", "text/html", "application/graphql", "multipart/form-data".'
+    And the JSON node "detail" should be equal to 'The content-type "text/plain" is not supported. Supported MIME types are "application/ld+json", "application/hal+json", "application/vnd.api+json", "application/xml", "text/xml", "application/json", "text/html", "application/graphql", "multipart/form-data".'
diff --git a/features/serializer/vo_relations.feature b/features/serializer/vo_relations.feature
index 63600d7b8..08999440a 100644
--- a/features/serializer/vo_relations.feature
+++ b/features/serializer/vo_relations.feature
@@ -150,18 +150,13 @@ Feature: Value object as ApiResource
           "type": "string",
           "pattern": "^hydra:Error$"
         },
-        "title": {
-          "type": "string",
-          "pattern": "^An error occurred$"
-        },
-        "description": {
+        "detail": {
           "pattern": "^Cannot create an instance of \"ApiPlatform\\\\Tests\\\\Fixtures\\\\TestBundle\\\\(Document|Entity)\\\\VoDummyCar\" from serialized data because its constructor requires the following parameters to be present : \"\\$drivers\".$"
         }
       },
       "required": [
         "@type",
-        "title",
-        "description"
+        "detail"
       ]
     }
     """
diff --git a/src/JsonLd/Serializer/ErrorNormalizer.php b/src/JsonLd/Serializer/ErrorNormalizer.php
index cfa6d062f..1227421a5 100644
--- a/src/JsonLd/Serializer/ErrorNormalizer.php
+++ b/src/JsonLd/Serializer/ErrorNormalizer.php
@@ -40,10 +40,14 @@ final class ErrorNormalizer implements NormalizerInterface
 
         if (isset($normalized['description'])) {
             $normalized['hydra:description'] = $normalized['description'];
+            unset($normalized['description']);
         }
 
         if (isset($normalized['title'])) {
             $normalized['hydra:title'] = $normalized['title'];
+            // this is confusing as the field is also available in the Problem Detail Json representation
+            // but we don't want to repeat the title in the response, tldr: use hydra without prefix
+            unset($normalized['title']);
         }
 
         return $normalized;


Test patch:
diff --git a/tests/Functional/ErrorTest.php b/tests/Functional/ErrorTest.php
index 99c10f1b3..ac8b3ae62 100644
--- a/tests/Functional/ErrorTest.php
+++ b/tests/Functional/ErrorTest.php
@@ -67,7 +67,7 @@ final class ErrorTest extends ApiTestCase
                 '401',
                 [
                     '@type' => 'hydra:Error',
-                    'description' => 'Unauthorized',
+                    'hydra:description' => 'Unauthorized',
                 ],
             ],
             [
diff --git a/tests/Functional/ItemUriTemplateTest.php b/tests/Functional/ItemUriTemplateTest.php
index 26c2430f4..ac4a6ea56 100644
--- a/tests/Functional/ItemUriTemplateTest.php
+++ b/tests/Functional/ItemUriTemplateTest.php
@@ -37,6 +37,6 @@ class ItemUriTemplateTest extends ApiTestCase
             'headers' => ['accept' => 'application/ld+json'],
         ]);
         $this->assertResponseStatusCodeSame(404);
-        $this->assertJsonContains(['description' => 'Not Found']);
+        $this->assertJsonContains(['detail' => 'Not Found']);
     }
 }
diff --git a/tests/JsonLd/Serializer/ErrorNormalizerTest.php b/tests/JsonLd/Serializer/ErrorNormalizerTest.php
index 916217d40..ebcfe23bc 100644
--- a/tests/JsonLd/Serializer/ErrorNormalizerTest.php
+++ b/tests/JsonLd/Serializer/ErrorNormalizerTest.php
@@ -28,8 +28,10 @@ final class ErrorNormalizerTest extends TestCase
         $res = $errorNormalizer->normalize(new \stdClass());
         $this->assertEquals('hydra:Error', $res['@type']);
         $this->assertArrayHasKey('hydra:description', $res);
-        $this->assertEquals($res['hydra:description'], $res['description']);
+        $this->assertEquals($res['hydra:description'], 'bar');
         $this->assertArrayHasKey('hydra:title', $res);
-        $this->assertEquals($res['hydra:title'], $res['title']);
+        $this->assertEquals($res['hydra:title'], 'foo');
+        $this->assertArrayNotHasKey('title', $res);
+        $this->assertArrayNotHasKey('description', $res);
     }
 }