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.