I have a content-type with an address field on it. The field is optional. I cannot send null as a value for that field or I get a 500 internal server error.

This happens on create (POST) or update (PATCH). I would expect to be able to remove such an optional field value from a node by sending a null value in a request.

I can successfully create a node if I pass in a valid address field or do not supply one at all. BUT in the case that I don't supply a value then the serialized response from the POST will contain the field attribute with a value of null. I would expect that if I changed an attribute value in such a result and send the content the server gave me directly back for an update (PATCH) it would succeed (say I just change the title of the node, the server should be able to deserialize what it serialized to me) but I can't because the server result contains null so my client needs to know to delete the field if the property is set and has a null value to avoid the 500 internal server error.

At this point it seems I simply cannot remove an optional address from the node since I cannot send an empty address (the server will complain that is invalid) and I cannot send null to have it completely removed. I haven't yet tried deleting the property on PATCH but this would go against the grain of the verb being a sparse UPDATE.

I.e. per the JSON API specification for PATCH

If a request does not include all of the attributes for a resource, the server MUST interpret the missing attributes as if they were included with their current values. The server MUST NOT interpret missing attributes as null values.

The stack trace:

Error: Call to a member function getClass() on null in /drupal/web/modules/contrib/jsonapi/src/Normalizer/FieldItemNormalizer.php on line 117 #0 /drupal/vendor/symfony/serializer/Serializer.php(182): Drupal\\jsonapi\\Normalizer\\FieldItemNormalizer->denormalize(NULL, 'Drupal\\\\address\\\\...', 'api_json', Array)\n#1 /drupal/web/modules/contrib/jsonapi/src/Serializer/Serializer.php(75): Symfony\\Component\\Serializer\\Serializer->denormalize(NULL, 'Drupal\\\\address\\\\...', 'api_json', Array)\n#2 /drupal/web/modules/contrib/jsonapi/src/Normalizer/FieldNormalizer.php(65): Drupal\\jsonapi\\Serializer\\Serializer->denormalize(NULL, 'Drupal\\\\address\\\\...', 'api_json', Array)\n#3 /drupal/vendor/symfony/serializer/Serializer.php(182): Drupal\\jsonapi\\Normalizer\\FieldNormalizer->denormalize(NULL, '\\\\Drupal\\\\Core\\\\Fi...', 'api_json', Array)\n#4 /drupal/web/modules/contrib/jsonapi/src/Serializer/Serializer.php(75): Symfony\\Component\\Serializer\\Serializer->denormalize(NULL, '\\\\Drupal\\\\Core\\\\Fi...', 'api_json', Array)\n#5 /drupal/web/modules/contrib/jsonapi/src/Normalizer/ContentEntityDenormalizer.php(77): Drupal\\jsonapi\\Serializer\\Serializer->denormalize(NULL, '\\\\Drupal\\\\Core\\\\Fi...', 'api_json', Array)\n#6 /drupal/web/modules/contrib/jsonapi/src/Normalizer/EntityDenormalizerBase.php(99): Drupal\\jsonapi\\Normalizer\\ContentEntityDenormalizer->prepareInput(Array, Object(Drupal\\jsonapi\\ResourceType\\ResourceType), 'api_json', Array)\n#7 /drupal/vendor/symfony/serializer/Serializer.php(182): Drupal\\jsonapi\\Normalizer\\EntityDenormalizerBase->denormalize(Array, 'Drupal\\\\node\\\\Ent...', 'api_json', Array)\n#8 /drupal/web/modules/contrib/jsonapi/src/Serializer/Serializer.php(75): Symfony\\Component\\Serializer\\Serializer->denormalize(Array, 'Drupal\\\\node\\\\Ent...', 'api_json', Array)\n#9 /drupal/web/modules/contrib/jsonapi/src/Normalizer/JsonApiDocumentTopLevelNormalizer.php(169): Drupal\\jsonapi\\Serializer\\Serializer->denormalize(Array, 'Drupal\\\\node\\\\Ent...', 'api_json', Array)\n#10 /drupal/vendor/symfony/serializer/Serializer.php(182): Drupal\\jsonapi\\Normalizer\\JsonApiDocumentTopLevelNormalizer->denormalize(Array, 'Drupal\\\\node\\\\Ent...', 'api_json', Array)\n#11 /drupal/web/modules/contrib/jsonapi/src/Serializer/Serializer.php(75): Symfony\\Component\\Serializer\\Serializer->denormalize(Array, 'Drupal\\\\jsonapi\\\\...', 'api_json', Array)\n#12 /drupal/web/modules/contrib/jsonapi/src/Controller/EntityResource.php(820): Drupal\\jsonapi\\Serializer\\Serializer->denormalize(Array, 'Drupal\\\\jsonapi\\\\...', 'api_json', Array)\n#13 /drupal/web/modules/contrib/jsonapi/src/Controller/EntityResource.php(306): Drupal\\jsonapi\\Controller\\EntityResource->deserialize(Object(Drupal\\jsonapi\\ResourceType\\ResourceType), Object(Symfony\\Component\\HttpFoundation\\Request), 'Drupal\\\\jsonapi\\\\...')\n#14 [internal function]: Drupal\\jsonapi\\Controller\\EntityResource->patchIndividual(Object(Drupal\\jsonapi\\ResourceType\\ResourceType), Object(Drupal\\node\\Entity\\Node), Object(Symfony\\Component\\HttpFoundation\\Request))\n#15 /drupal/web/core/lib/Drupal/Core/EventSubscriber/EarlyRenderingControllerWrapperSubscriber.php(123): call_user_func_array(Array, Array)\n#16 /drupal/web/core/lib/Drupal/Core/Render/Renderer.php(582): Drupal\\Core\\EventSubscriber\\EarlyRenderingControllerWrapperSubscriber->Drupal\\Core\\EventSubscriber\\{closure}()\n#17 /drupal/web/core/lib/Drupal/Core/EventSubscriber/EarlyRenderingControllerWrapperSubscriber.php(124): Drupal\\Core\\Render\\Renderer->executeInRenderContext(Object(Drupal\\Core\\Render\\RenderContext), Object(Closure))\n#18 /drupal/web/core/lib/Drupal/Core/EventSubscriber/EarlyRenderingControllerWrapperSubscriber.php(97): Drupal\\Core\\EventSubscriber\\EarlyRenderingControllerWrapperSubscriber->wrapControllerExecutionInRenderContext(Array, Array)\n#19 /drupal/vendor/symfony/http-kernel/HttpKernel.php(151): Drupal\\Core\\EventSubscriber\\EarlyRenderingControllerWrapperSubscriber->Drupal\\Core\\EventSubscriber\\{closure}()\n#20 /drupal/vendor/symfony/http-kernel/HttpKernel.php(68): Symfony\\Component\\HttpKernel\\HttpKernel->handleRaw(Object(Symfony\\Component\\HttpFoundation\\Request), 1)\n#21 /drupal/web/core/lib/Drupal/Core/StackMiddleware/Session.php(57): Symfony\\Component\\HttpKernel\\HttpKernel->handle(Object(Symfony\\Component\\HttpFoundation\\Request), 1, true)\n#22 /drupal/web/core/lib/Drupal/Core/StackMiddleware/KernelPreHandle.php(47): Drupal\\Core\\StackMiddleware\\Session->handle(Object(Symfony\\Component\\HttpFoundation\\Request), 1, true)\n#23 /drupal/web/core/modules/page_cache/src/StackMiddleware/PageCache.php(99): Drupal\\Core\\StackMiddleware\\KernelPreHandle->handle(Object(Symfony\\Component\\HttpFoundation\\Request), 1, true)\n#24 /drupal/web/core/modules/page_cache/src/StackMiddleware/PageCache.php(78): Drupal\\page_cache\\StackMiddleware\\PageCache->pass(Object(Symfony\\Component\\HttpFoundation\\Request), 1, true)\n#25 /drupal/web/modules/contrib/jsonapi/src/StackMiddleware/FormatSetter.php(45): Drupal\\page_cache\\StackMiddleware\\PageCache->handle(Object(Symfony\\Component\\HttpFoundation\\Request), 1, true)\n#26 /drupal/web/core/lib/Drupal/Core/StackMiddleware/ReverseProxyMiddleware.php(47): Drupal\\jsonapi\\StackMiddleware\\FormatSetter->handle(Object(Symfony\\Component\\HttpFoundation\\Request), 1, true)\n#27 /drupal/web/core/lib/Drupal/Core/StackMiddleware/NegotiationMiddleware.php(52): Drupal\\Core\\StackMiddleware\\ReverseProxyMiddleware->handle(Object(Symfony\\Component\\HttpFoundation\\Request), 1, true)\n#28 /drupal/vendor/stack/builder/src/Stack/StackedHttpKernel.php(23): Drupal\\Core\\StackMiddleware\\NegotiationMiddleware->handle(Object(Symfony\\Component\\HttpFoundation\\Request), 1, true)\n#29 /drupal/web/core/lib/Drupal/Core/DrupalKernel.php(693): Stack\\StackedHttpKernel->handle(Object(Symfony\\Component\\HttpFoundation\\Request), 1, true)\n#30 /drupal/web/index.php(19): Drupal\\Core\\DrupalKernel->handle(Object(Symfony\\Component\\HttpFoundation\\Request))

I'm upgrading from 1x and I must say I'm actually running into a lot of 500 internal server errors relating to property definitions within complex fields. So far I've been adding debug code to FieldItemNormalizer to figure out what's causing the server to choke and working around issues like this.

In this case if I add a few dd statements in the offending code:

public function denormalize($data, $class, $format = NULL, array $context = []) {
...
dd($class);
    if (!is_array($data)) {
      $property_value = $data;
      $property_name = $item_definition->getMainPropertyName();
dd($property_name.'='.isset($property_definitions[$property_name]));
      $property_value_class = $property_definitions[$property_name]->getClass();
      return $denormalize_property($property_name, $property_value, $property_value_class, $format, $context);
    }
...
}

The resulting output is:
Drupal\address\Plugin\Field\FieldType\AddressItem
followed by
=
so $property_name is empty as is its value in $property_definitions.

CommentFileSizeAuthor
#43 3048348-42-8.7--test-only-FAIL.patch3.61 KBSpokje
#43 interdiff_test_only_28_42.txt759 bytesSpokje
#42 interdiff_33_42.txt759 bytesSpokje
#42 3048348-42-8.7.patch4.6 KBSpokje
#33 3048348-33-8.7.patch4.57 KBWim Leers
#28 3048348-28.patch4.56 KBWim Leers
#28 3048348-28--tests-only-FAIL.patch3.57 KBWim Leers
#5 3048348-5.patch1.72 KBWim Leers
#6 interdiff.txt2.12 KBWim Leers
#6 3048348-6.patch3.5 KBWim Leers
#7 interdiff.txt1005 bytesWim Leers
#7 3048348-7.patch4.46 KBWim Leers
#10 3048348-9.patch4.64 KBWim Leers
#15 3048348-15.patch4.43 KByogeshmpawar
#21 interdiff.txt992 bytesWim Leers
#21 3048348-21.patch4.43 KBWim Leers
#21 3048348-21--tests-only-FAIL.patch3.45 KBWim Leers
#23 3048348-23.patch4.45 KBlogickal
#27 interdiff.txt1.99 KBWim Leers
#27 3048348-27--tests-only-FAIL.patch3.57 KBWim Leers
#27 3048348-27.patch4.56 KBWim Leers
Support from Acquia helps fund testing for Drupal Acquia logo

Comments

adamspe created an issue. See original summary.

adamspe’s picture

Title: 500 Internal Server Error on set optional address field to null (POST || PATCH) » 500 Internal Server Error on set optional complex field to null (POST || PATCH)
Priority: Normal » Major

I should clarify that this issue is not specific to address fields. It applies, not surprisingly, to any complex field. I similarly have a geo location field that is optional. If I try to null it out the same error occurs and the class name from the dd statement mentioned in the original description simply changes to:

Drupal\geolocation\Plugin\Field\FieldType\GeolocationItem

Wim Leers’s picture

Title: 500 Internal Server Error on set optional complex field to null (POST || PATCH) » Denormalizing NULL for an optional @FieldType=address field fails
Priority: Major » Normal
Issue tags: +API-First Initiative, +Entity Field API
Related issues: +#2823536: AddressItem should override FieldItemBase::mainPropertyName()

I would expect that if I changed an attribute value in such a result and send the content the server gave me directly back for an update (PATCH) it would succeed (say I just change the title of the node, the server should be able to deserialize what it serialized to me)

Absolutely! We even have explicit test coverage for this.

Error: Call to a member function getClass() on null in /drupal/web/modules/contrib/jsonapi/src/Normalizer/FieldItemNormalizer.php on line 117
…

That's this code:

    if (!is_array($data)) {
      $property_value = $data;
      $property_name = $item_definition->getMainPropertyName();
      $property_value_class = $property_definitions[$property_name]->getClass();
      return $denormalize_property($property_name, $property_value, $property_value_class, $format, $context);
    }

Which field type are we talking about? It looks like its main property name doesn't match an existing property on the field, which would be a bug in that field type. I'll only know for sure if I can look at the code for the field type. Ah you already dug into exactly that, great!

The problem is now obviously a mismatch between the expectations of JSON:API's normalizer (which assumes there will be a main property) and the implementation of \Drupal\address\Plugin\Field\FieldType\AddressItem::mainPropertyName():

  public static function mainPropertyName() {
    return NULL;
  }

(which clearly is not designating a particular property to be the main one — this was a change made in #2823536: AddressItem should override FieldItemBase::mainPropertyName()).

Wim Leers’s picture

Title: Denormalizing NULL for an optional @FieldType=address field fails » Denormalizing NULL for an optional @FieldType=address or @FieldType=geolocation field fails due to either no main property name or computed read-only main property
Related issues: +#2907560: Add support for search_api_location indexing

#1: ah, but there is a crucial difference: @FieldType=geolocation does have

  public static function mainPropertyName() {
    return 'value';
  }

but it also has:

  public static function propertyDefinitions(FieldStorageDefinitionInterface $field_definition) {
…
    $properties['value'] = DataDefinition::create('string')
      ->setLabel(t('Computed lat,lng value'))
      ->setComputed(TRUE)
      ->setInternal(FALSE)
      ->setClass(GeolocationComputed::class);

    return $properties;
  }

(which was introduced in #2907560: Add support for search_api_location indexing).

So in this case, while the symptom is similar, it has a different cause: a main property exists, but it's computed and read-only.

Wim Leers’s picture

Version: 8.x-2.4 » 8.x-2.x-dev
Assigned: Unassigned » Wim Leers
Status: Active » Needs review
FileSize
1.72 KB

First, let's add test coverage verifying that fields like @FieldType=address or @FieldType=geolocation indeed normalize to null when they're empty/not set. We can't test with either of those fields since they're in contrib rather than in core, but we can fortunately approximate this using the map field type. And in fact, we already have test coverage for the map field type that we can simply expand :)

Wim Leers’s picture

Next, let's add explicit test coverage for the scenario you described: an entity with an optional/empty @FieldType=map where only a different field (such as the label field) is being modified should result in a successful PATCH request without any other request body modifications.

If this fails, this is successfully reproducing the problem reported by @adamspe!

Wim Leers’s picture

Assigned: Wim Leers » Unassigned
FileSize
1005 bytes
4.46 KB

Here's the fix.

The last submitted patch, 6: 3048348-6.patch, failed testing. View results

gabesullice’s picture

Status: Needs review » Reviewed & tested by the community

Thanks! This looks good to me.

Wim Leers’s picture

Project: JSON:API » Drupal core
Version: 8.x-2.x-dev » 8.8.x-dev
Component: Code » jsonapi.module
FileSize
4.64 KB

Great! Then porting this to a core patch that should be committed first.

Status: Reviewed & tested by the community » Needs work

The last submitted patch, 10: 3048348-9.patch, failed testing. View results

Wim Leers’s picture

Status: Needs work » Reviewed & tested by the community

Looks like a random fail. Retesting.

alexpott’s picture

Status: Reviewed & tested by the community » Needs work

Needs a reroll.

yogeshmpawar’s picture

Assigned: Unassigned » yogeshmpawar
yogeshmpawar’s picture

Assigned: yogeshmpawar » Unassigned
Status: Needs work » Needs review
FileSize
4.43 KB

Rerolled the #10 patch.

Wim Leers’s picture

Status: Needs review » Reviewed & tested by the community

Diffed both patches, they're making the same changes, only context has changed. 👍

Thanks, @yogeshmpawar! 🙏

alexpott’s picture

Crediting @adamspe for filing the issue.

alexpott’s picture

Status: Reviewed & tested by the community » Needs work
+++ b/core/modules/jsonapi/tests/src/Functional/JsonApiRegressionTest.php
@@ -990,4 +996,44 @@ public function testLeakedCacheMetadataViaRdfFromIssue3053827() {
+      RequestOptions::AUTH => [$user->getUsername(), $user->pass_raw],

This is using deprecated code but we don't know that because the test is erroneously marked as @group legacy. Should be getAccountName()

+++ b/core/modules/jsonapi/tests/src/Functional/JsonApiRegressionTest.php
@@ -990,4 +996,44 @@ public function testLeakedCacheMetadataViaRdfFromIssue3053827() {
+  /**
+   * Ensure optional `@FieldType=map` fields are denormalized correctly.
+   *
+   * @see https://www.drupal.org/project/jsonapi/issues/3048348
+   */
+  public function testEmptyMapFieldTypeDenormalizationFromIssue3048348() {

Let's remove the issue ID from the test name and the @see. This is not normal practice for core and I don';t think it should be. git blame is our friend.

Furthermore we really need another issue to remove the @group legacy and tidy this test up at bit and probably rename it. This test and all the others in this file are not regression tests as far as I know and I'm not sure why we care whether a test is for a regression or not. Once you have the test and fix in its a bug and test for that bugfix.

alexpott’s picture

garphy’s picture

Status: Needs work » Needs review

Now that #3042745: Remove group @legacy from jsonapi tests and fix deprecation messages is closed, get back to reviewing this one.

Wim Leers’s picture

Status: Needs review » Reviewed & tested by the community
FileSize
992 bytes
4.43 KB
3.45 KB

Thanks @garphy :)

#18:

  1. Fixed.
  2. While atypical, this is not the only place. \Drupal\Tests\big_pipe\FunctionalJavascript\BigPipeRegressionTest is the other example.

    git blame doesn't work when code has been imported from contrib to core, because we don't retain those commit histories.

    These really are regression tests. We have a tendency to make even tests abstract/generalized. And yes, REST/JSON:API tests are extra super guilty of this, but in that case it's out of necessity. Having these tests tied to very specific bug reports helps keep the tests simple and understandable, resist that temptation. I am the first to agree this is atypical! But it's an experiment, that so far seems to be working out quite well.
    Generally speaking, if you need a very particular combination of configuration/setup, we'll add a focused test method to JsonApiRegressionTest. If it's a generic bug that can reasonably be tested in an abstract way across all entity types, we'll add test coverage to \Drupal\Tests\jsonapi\Functional\ResourceTestBase.

Wim Leers’s picture

Status: Reviewed & tested by the community » Needs work
Issue tags: +Needs reroll

D'oh, recent core commits made #21 not apply! Anybody care to reroll? :)

logickal’s picture

Status: Needs work » Needs review
Issue tags: -Needs reroll
FileSize
4.45 KB

Here's a quick re-roll.

Wim Leers’s picture

Status: Needs review » Reviewed & tested by the community

Perfect, thanks!

larowlan’s picture

Status: Reviewed & tested by the community » Needs review
+++ b/core/modules/jsonapi/tests/src/Functional/JsonApiRegressionTest.php
@@ -1085,4 +1091,44 @@ public function testInvalidDataTriggersUnprocessableEntityErrorFromIssue3052954(
+   * @see https://www.drupal.org/project/jsonapi/issues/3048348
...
+  public function testEmptyMapFieldTypeDenormalizationFromIssue3048348() {

Can we remove the issue reference here (I realise there are existing ones, but let's not add new ones) - we can use git blame to work out when this was added.

larowlan’s picture

Damn, I didn't save my comment properly - also had this observation:

+++ b/core/modules/jsonapi/tests/src/Functional/JsonApiRegressionTest.php
@@ -1085,4 +1091,44 @@ public function testInvalidDataTriggersUnprocessableEntityErrorFromIssue3052954(
+    $this->assertSame(200, $response->getStatusCode());

should we assert that the returned values are unchanged for the map field, per the issue summary that is also a problem in HEAD for these fields?

Wim Leers’s picture

Status: Needs review » Reviewed & tested by the community
FileSize
4.56 KB
3.57 KB
1.99 KB

Done and done!

Wim Leers’s picture

+++ b/core/modules/jsonapi/tests/src/Functional/JsonApiRegressionTest.php
@@ -1129,6 +1128,7 @@ public function testEmptyMapFieldTypeDenormalizationFromIssue3048348() {
+    $this->assertSame($doc['data']['attributes']['data'], Json::decode((string) $response->getBody()['data']['attributes']['data']);

This contained a typo because I forgot to stage a change 🙃 😊

The last submitted patch, 28: 3048348-28--tests-only-FAIL.patch, failed testing. View results

larowlan’s picture

  • larowlan committed 6aec5a3 on 8.8.x
    Issue #3048348 by Wim Leers, yogeshmpawar, logickal, alexpott, adamspe:...
larowlan’s picture

Version: 8.8.x-dev » 8.7.x-dev
Status: Reviewed & tested by the community » Patch (to be ported)

Committed 6aec5a3 and pushed to 8.8.x. Thanks!

Wim Leers’s picture

Status: Patch (to be ported) » Reviewed & tested by the community
FileSize
4.57 KB

Ported :)

gambry’s picture

Can last commit be causing https://www.drupal.org/pift-ci-job/1390858 ?

1) Drupal\Tests\jsonapi\Functional\JsonApiRegressionTest::testMapFieldTypeNormalizationFromIssue3040590
Failed asserting that null is identical to Array &0 (
'foo' => 'bar'
).

/var/www/html/core/modules/jsonapi/tests/src/Functional/JsonApiRegressionTest.php:913

gambry’s picture

Yes it looks like it does.

larowlan’s picture

Do we have a followup for the postgres fail or should I just revert this?

gambry’s picture

I planned to do some investigation to answer exactly that question, but haven't had the time yet.

I don't see how the changes in here can be the direct cause for the PostrgreSQL issue, besides fixing the real problem can not be that quick.
So either we should revert this or not, worth fixing the failure is its own issue.

I'll create one and link in here.

gambry’s picture

@gabesullice found and fixed the issue on #3078639: PostgreSQL automated test failing for JsonApiRegressionTest after latest changes on json_api FieldItemNormalizer, but I think the window for last 8.7.x patch releases is now gone?
If that's the case, then this issue can now close.

Wim Leers’s picture

but I think the window for last 8.7.x patch releases is now gone?

No, it is not. 8.8.0's release is still months away (December 4, 2019), we still want bugfixes in the mean time :D

Wim Leers’s picture

Status: Reviewed & tested by the community » Needs work

But that does mean we need this patch to be rerolled for 8.7, to include the Postgres fix that is RTBC at #3078639: PostgreSQL automated test failing for JsonApiRegressionTest after latest changes on json_api FieldItemNormalizer.

Spokje’s picture

Spokje’s picture

Spokje’s picture

Status: Needs work » Needs review

Not quite sure if combining to RTBC issues with green Testbot result and having it done myself qualifies me to put this back on RTBC, so Needs Review it will be.

gambry’s picture

Status: Needs review » Reviewed & tested by the community

All looks good. Indeed #42 is #33 + the PostgreSQL fix already in 8.8.x branch with #3078639: PostgreSQL automated test failing for JsonApiRegressionTest after latest changes on json_api FieldItemNormalizer. So this is RTBC.

larowlan’s picture

Hey folks, which patch is RTBC here for 8.7.x - chance we could upload it as the last one - I'm kind of lost

Spokje’s picture

@larowlan Patch in #42 is the D8.7 patch

  • larowlan committed 93ddb31 on 8.7.x
    Issue #3048348 by Wim Leers, Spokje, yogeshmpawar, logickal, alexpott,...
larowlan’s picture

Status: Reviewed & tested by the community » Fixed

thanks!

Committed 93ddb31 and pushed to 8.7.x. Thanks!

Status: Fixed » Closed (fixed)

Automatically closed - issue fixed for 2 weeks with no activity.