Problem/Motivation
JSON:API has a nice feature in that it flattens a Drupal field value so we don't have pesky value
sub-properties on things like string, boolean, email, etc fields. This is great, except when you want your field type to have a forced sub-property.
In the Commerce API module being developed we have a billing_information
field that has at least an address
property and may have more, such as a tax number or phone number.
Currently, the billing_information
field just displays all of the address item values as root properties, causing a broken schema.
The relevant code is: https://git.drupalcode.org/project/drupal/blob/8.8.x/core/modules/jsonap...
$field_properties = TypedDataInternalPropertiesHelper::getNonInternalProperties($field_item);
// Flatten if there is only a single property to normalize.
$values = static::rasterizeValueRecursive(count($field_properties) == 1 ? reset($values) : $values);
getNonInternalProperties
will return properties that are not computed or marked internal.
I had to make this workaround for myself:
/**
* {@inheritdoc}
*/
public function normalize($object, $format = NULL, array $context = []) {
assert($object instanceof Address);
// Work around for JSON:API's normalization of FieldItems. If there is only
// one root property in the field item, it will flatten the values. We do
// not want that for the OrderProfile field, as `address` should be present.
// This only happens if there is one field on the profile.
// @see \Drupal\jsonapi\Normalizer\FieldItemNormalizer::normalize
// @todo open issue FieldItemNormalizer::normalize should ignore if mainPropertyName is NULL.
$parent = $object->getParent();
if ($parent instanceof OrderProfile) {
$field_properties = TypedDataInternalPropertiesHelper::getNonInternalProperties($parent);
if (count($field_properties) === 1) {
// This ensures the value is always under an `address` property.
return ['address' => array_filter($object->getValue())];
}
}
return array_filter($object->getValue());
}
Proposed resolution
If there is one property returned by TypedDataInternalPropertiesHelper::getNonInternalProperties
, check if it matches the main property value. In most cases it will, or getMainPropertyName
wil be NULL. If it doesn't match, then do not flatten the field values. Often times getMainPropertyName returns NULL If the field type has multiple properties for its value and there cannot be a single property used (ie: price field, you need the number and currency.)
Remaining tasks
User interface changes
None.
API changes
JSON:API will no longer flatten field types which return NULL or a main property name which does not match the single property returned.
Data model changes
None.
Release notes snippet
Comment | File | Size | Author |
---|---|---|---|
#30 | interdiff_28-30.txt | 2.97 KB | johnwebdev |
#30 | 3112229-30.patch | 8.75 KB | johnwebdev |
#28 | fielditemnormalizer-3112229-28.patch | 9.94 KB | mglaman |
#17 | fielditemnormalizer-3112229-17.patch | 9.94 KB | mglaman |
#17 | interdiff-3112229-11-17.txt | 838 bytes | mglaman |
Comments
Comment #2
mglamanPatch. no tests.
Comment #3
mglamanComment #4
xjmThanks for filing this!
Only D9-only bug fixes and major-only allowed changes during the beta phase should be filed against 9.0.x. New features, API additions, and deprecations should be filed against 9.1.x.
So, moving to the 9.1.x-dev branch.
Comment #5
Kristen Pol@mglaman What's a good way to manually test this patch?
Comment #6
mglaman@Kristen Pol, unfortunately, there isn't unless you see it's working for us in commerce_api. I'm going to work on a test that adds the prerequisite field type plugin.
Comment #7
mglamanWorking on tests for this now
Comment #8
mglamanHere are tests that add coverage for FieldItemNormalizer:
Comment #9
jibranThis is a bug, IMO. Let's have a test only patch as well.
Given that we only flatten a single property field what about the fields with multiple properties but still has mainProperty. Do we want to test that here as well or would that be out of scope here?
These should be added to the entity test module so that other modules can also use them if needs be.
Comment #10
mglamanThere isn't a strong opinion in core on what to do. If there are multiple properties, they should all be returned. The main property is used to say "if you absolutely need a value, this is the one. For instance, the Price field in Commerce does not define the main property, as a price value is useless without the number and currency.
I see setting the main property to NULL as specifying that the value cannot be simply derived through a single property.
Can do!
Putting to NW so I can roll a test-only patch, and address #9.2
Comment #11
mglamanAddresses #9.2, and here is a test-only patch.
Comment #14
mglamanFailures were completely unrelated, queueing the tests again.
Comment #17
mglaman\Drupal\Tests\field\Kernel\FieldTypePluginManagerTest::testMainProperty doesn't respect the fact
mainPropertyName
may be null.Comment #19
jibranMy feedback has beend addressed and patch looks great to me so setting it to RTBC.
WoW, I didn't know that this is amazing.
Comment #20
jibranComment #22
mglaman🤔 is there a way to tell the test bot to not retest the test only patch every two days?
Comment #24
jibranYes, if you upload test only patch first and the actual patch as last. The order matters here.
Comment #25
mglaman😐 good to know, thanks @jibran
Comment #28
mglamanRe-uploading so it stops re-testing the test-only patch
Comment #29
Wim Leers🤓 We don't need to specify all of these.
🤓 Missing
@inheritdoc
?🤔 "has no properties returned"
🤓 Why not
assertSame()
?🐛 🤓 with?
👍 These are adding explicit unit (well, kernel) tests for pre-existing logic. That's why these assertions pass even in the test-only patch.
👍 This is testing the fix.
🤔 Why not just reuse
\Drupal\Core\Field\Plugin\Field\FieldType\MapItem
?Comment #30
johnwebdev CreditAttribution: johnwebdev commented#29.1 Fixed
2. Fixed
3. Fixed (I think 😅)
4. Fixed
5. We're asserting multiple cases here, so changed the method name and description to be more generic.
6. Makes sense! Let's do that.
Comment #31
mglaman+1 thanks for fixed, @johnwebdev!
Comment #32
Wim Leers🚢
Comment #34
Wim LeersRandom/unrelated fail in
1) Drupal\Tests\quickedit\FunctionalJavascript\QuickEditIntegrationTest::testCustomBlock
.Comment #37
raman.b CreditAttribution: raman.b at OpenSense Labs commentedMoving back to RTBC; unrelated test failure.
Comment #40
catchCommitted/pushed to 9.2.x and cherry-picked to 9.1.x.
Marking this fixed against 9.1.x - if you feel strongly this should be backported to 8.9.x please re-open, but it's not clear to me if a client might potentially be relying on the existing broken behaviour.