diff --git a/core/modules/jsonapi/src/Normalizer/FieldItemNormalizer.php b/core/modules/jsonapi/src/Normalizer/FieldItemNormalizer.php index 8a66733839..183fac0730 100644 --- a/core/modules/jsonapi/src/Normalizer/FieldItemNormalizer.php +++ b/core/modules/jsonapi/src/Normalizer/FieldItemNormalizer.php @@ -112,6 +112,12 @@ public function denormalize($data, $class, $format = NULL, array $context = []) // be expanded to an array of all properties, we special-case single-value // properties. if (!is_array($data)) { + // The NULL normalization means there is no value, hence we can return + // early. Note that this is not just an optimization but a necessity for + // field types without main properties (such as the "map" field type). + if ($data === NULL) { + return $data; + } $property_value = $data; $property_name = $item_definition->getMainPropertyName(); $property_value_class = $property_definitions[$property_name]->getClass(); diff --git a/core/modules/jsonapi/tests/src/Functional/JsonApiRegressionTest.php b/core/modules/jsonapi/tests/src/Functional/JsonApiRegressionTest.php index 3fc3c5a183..a4d146c727 100644 --- a/core/modules/jsonapi/tests/src/Functional/JsonApiRegressionTest.php +++ b/core/modules/jsonapi/tests/src/Functional/JsonApiRegressionTest.php @@ -875,19 +875,24 @@ public function testMapFieldTypeNormalizationFromIssue3040590() { $this->assertTrue($this->container->get('module_installer')->install(['entity_test'], TRUE), 'Installed modules.'); // Create data. - $entity = EntityTestMapField::create([ + $entity_a = EntityTestMapField::create([ + 'name' => 'A', 'data' => [ 'foo' => 'bar', 'baz' => 'qux', ], ]); - $entity->save(); + $entity_a->save(); + $entity_b = EntityTestMapField::create([ + 'name' => 'B', + ]); + $entity_b->save(); $user = $this->drupalCreateUser([ 'administer entity_test content', ]); // Test. - $url = Url::fromUri(sprintf('internal:/jsonapi/entity_test_map_field/entity_test_map_field', $entity->uuid())); + $url = Url::fromUri('internal:/jsonapi/entity_test_map_field/entity_test_map_field'); $request_options = [ RequestOptions::AUTH => [$user->getAccountName(), $user->pass_raw], ]; @@ -898,7 +903,8 @@ public function testMapFieldTypeNormalizationFromIssue3040590() { 'foo' => 'bar', 'baz' => 'qux', ], $data['data'][0]['attributes']['data']); - $entity->set('data', [ + $this->assertNull($data['data'][1]['attributes']['data']); + $entity_a->set('data', [ 'foo' => 'bar', ])->save(); $response = $this->request('GET', $url, $request_options); @@ -1085,4 +1091,44 @@ public function testInvalidDataTriggersUnprocessableEntityErrorFromIssue3052954( $this->assertSame(422, $response->getStatusCode()); } + /** + * Ensure optional `@FieldType=map` fields are denormalized correctly. + */ + public function testEmptyMapFieldTypeDenormalization() { + $this->config('jsonapi.settings')->set('read_only', FALSE)->save(TRUE); + + // Set up data model. + $this->assertTrue($this->container->get('module_installer')->install(['entity_test'], TRUE), 'Installed modules.'); + + // Create data. + $entity = EntityTestMapField::create([ + 'name' => 'foo', + ]); + $entity->save(); + $user = $this->drupalCreateUser([ + 'administer entity_test content', + ]); + + // Test. + $url = Url::fromUri(sprintf('internal:/jsonapi/entity_test_map_field/entity_test_map_field/%s', $entity->uuid())); + $request_options = [ + RequestOptions::AUTH => [$user->getAccountName(), $user->pass_raw], + ]; + // Retrieve the current representation of the entity. + $response = $this->request('GET', $url, $request_options); + $this->assertSame(200, $response->getStatusCode()); + $doc = Json::decode((string) $response->getBody()); + // Modify the title. The @FieldType=map normalization is not changed. (The + // name of this field is confusingly also 'data'.) + $doc['data']['attributes']['name'] = 'bar'; + $request_options[RequestOptions::HEADERS] = [ + 'Content-Type' => 'application/vnd.api+json', + 'Accept' => 'application/vnd.api+json', + ]; + $request_options[RequestOptions::BODY] = Json::encode($doc); + $response = $this->request('PATCH', $url, $request_options); + $this->assertSame(200, $response->getStatusCode()); + $this->assertSame($doc['data']['attributes']['data'], Json::decode((string) $response->getBody()['data']['attributes']['data']); + } + }