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.
Comment | File | Size | Author |
---|---|---|---|
#43 | 3048348-42-8.7--test-only-FAIL.patch | 3.61 KB | Spokje |
#43 | interdiff_test_only_28_42.txt | 759 bytes | Spokje |
#42 | interdiff_33_42.txt | 759 bytes | Spokje |
#42 | 3048348-42-8.7.patch | 4.6 KB | Spokje |
#33 | 3048348-33-8.7.patch | 4.57 KB | Wim Leers |
Comments
Comment #2
adamspe CreditAttribution: adamspe commentedI 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 thedd
statement mentioned in the original description simply changes to:Drupal\geolocation\Plugin\Field\FieldType\GeolocationItem
Comment #3
Wim LeersAbsolutely! We even have explicit test coverage for this.
That's this code:
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()
:(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()).
Comment #4
Wim Leers#1: ah, but there is a crucial difference:
@FieldType=geolocation
does havebut it also has:
(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.
Comment #5
Wim LeersFirst, let's add test coverage verifying that fields like
@FieldType=address
or@FieldType=geolocation
indeed normalize tonull
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 themap
field type. And in fact, we already have test coverage for the map field type that we can simply expand :)Comment #6
Wim LeersNext, 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 successfulPATCH
request without any other request body modifications.If this fails, this is successfully reproducing the problem reported by @adamspe!
Comment #7
Wim LeersHere's the fix.
Comment #9
gabesulliceThanks! This looks good to me.
Comment #10
Wim LeersGreat! Then porting this to a core patch that should be committed first.
Comment #12
Wim LeersLooks like a random fail. Retesting.
Comment #13
alexpottNeeds a reroll.
Comment #14
yogeshmpawarComment #15
yogeshmpawarRerolled the #10 patch.
Comment #16
Wim LeersDiffed both patches, they're making the same changes, only context has changed. 👍
Thanks, @yogeshmpawar! 🙏
Comment #17
alexpottCrediting @adamspe for filing the issue.
Comment #18
alexpottThis is using deprecated code but we don't know that because the test is erroneously marked as @group legacy. Should be getAccountName()
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.
Comment #19
alexpottI.e. let's fix #3042745: Remove group @legacy from jsonapi tests and fix deprecation messages first.
Comment #20
garphy CreditAttribution: garphy at ICI LA LUNE commentedNow that #3042745: Remove group @legacy from jsonapi tests and fix deprecation messages is closed, get back to reviewing this one.
Comment #21
Wim LeersThanks @garphy :)
#18:
\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
.Comment #22
Wim LeersD'oh, recent core commits made #21 not apply! Anybody care to reroll? :)
Comment #23
logickal CreditAttribution: logickal at The Weather Company commentedHere's a quick re-roll.
Comment #24
Wim LeersPerfect, thanks!
Comment #25
larowlanCan 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.
Comment #26
larowlanDamn, I didn't save my comment properly - also had this observation:
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?
Comment #27
Wim LeersDone and done!
Comment #28
Wim LeersThis contained a typo because I forgot to stage a change 🙃 😊
Comment #30
larowlanComment #32
larowlanCommitted 6aec5a3 and pushed to 8.8.x. Thanks!
Comment #33
Wim LeersPorted :)
Comment #34
gambryCan last commit be causing https://www.drupal.org/pift-ci-job/1390858 ?
Comment #35
gambryYes it looks like it does.
Comment #36
larowlanDo we have a followup for the postgres fail or should I just revert this?
Comment #37
gambryI 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.
Comment #38
gambry#3078639: PostgreSQL automated test failing for JsonApiRegressionTest after latest changes on json_api FieldItemNormalizer
Comment #39
gambry@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.
Comment #40
Wim LeersNo, it is not. 8.8.0's release is still months away (December 4, 2019), we still want bugfixes in the mean time :D
Comment #41
Wim LeersBut 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.
Comment #42
SpokjeIncluded the 8.8 Postgres fix into #33
Comment #43
SpokjeTest-only with D8.8 postgres fix included.
Comment #44
SpokjeNot 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.
Comment #45
gambryAll 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.
Comment #46
larowlanHey folks, which patch is RTBC here for 8.7.x - chance we could upload it as the last one - I'm kind of lost
Comment #47
Spokje@larowlan Patch in #42 is the D8.7 patch
Comment #49
larowlanthanks!
Committed 93ddb31 and pushed to 8.7.x. Thanks!