Problem/Motivation

Discovered while working on #2930028: Comprehensive JSON API integration test coverage phase 1: for every entity type, individual resources only.

Quoting #2930028-55: Comprehensive JSON API integration test coverage phase 1: for every entity type, individual resources only:

While working on the Block test coverage, I found a significant bug in the normalization of config entities. #2733097: [FEATURE] Add GET support for configuration entities added config entity GET support, but it did not add any integration test coverage. The only test coverage is \Drupal\Tests\jsonapi\Kernel\Normalizer\JsonApiDocumentTopLevelNormalizerTest::testNormalizeConfig(), but that didn't verify that a config entity's dependencies key is exposed correctly.

This is what we'd expect:

…
          'dependencies' => [
            'theme' => [
              'classy',
            ],
          ],
…

but in reality we get:

…
          'dependencies' => [
            'classy',
          ],
…

Obviously that's losing very important information!

\Drupal\jsonapi\Normalizer\ConfigEntityNormalizer::serializeField() does:

    $output = new FieldNormalizerValue(
      [new FieldItemNormalizerValue($field)],
      1
    );

That 1 means "single cardinality" … which during rasterization results in \Drupal\jsonapi\Normalizer\Value\FieldNormalizerValue::rasterizeValue() being called and this code executing:

    if ($this->cardinality == 1) {
      assert(count($this->values) === 1);
      return $this->values[0] instanceof FieldItemNormalizerValue
        ? $this->values[0]->rasterizeValue() : NULL;
    }

… which then results in only the first value being retained, and this explains the data loss shown above.

Proposed resolution

Fix this in ConfigEntityNormalizer. Details TBD.

Remaining tasks

TBD

User interface changes

None.

API changes

None. (Well, responses for config entities change, but it was impossible until this very issue to use anything under the dependencies key due to this bug!)

Data model changes

None.

Comments

Wim Leers created an issue. See original summary.

wim leers’s picture

Issue summary: View changes
e0ipso’s picture

Wow! That was a good catch. We should add this issue to our current focus points.

wim leers’s picture

Ran into this again in #2932031-24: Comprehensive JSON API integration test coverage phase 2: for every entity type, complex use-cases and #2932031-28: Comprehensive JSON API integration test coverage phase 2: for every entity type, complex use-cases.

I wrote

The fix is to not just check whether there's >1 value in the array, but whether it's a numerically indexed array or an associatively indexed array. In the latter case, indexes/keys have meaning, and shouldn't be dropped!

But @gabesullice correctly pointed out that that doesn't work:

I think that's still a little too naive and will break some lauded JSON API DX improvements. We want to check (1) the cardinality of the field and (2) whether its data definition is a sequence or a map.

A sequence w/ cardinality = 1 -> strip wrapper
A sequence w/ cardinality > 1 -> keep wrapper
A map                         -> keep wrapper

To which I replied:

Except there are no data definitions for config entity types.

Which brings me to what I think we actually need to do: special case config entities and just make it output exactly what is stored, no attempts to simplify the data structure can be made.

gabesullice’s picture

Which brings me to what I think we actually need to do: special case config entities and just make it output exactly what is stored, no attempts to simplify the data structure can be made.

+1

Edit: Actually, don't they have data definitions? I thought that they did have definitions when derived from a value, you just can't get them out of thin air.

I.e.

$definition = $config_entity->getTypedData()->getDefinition(); // works
$definition = DataDefinition::createFromDataType($config_entity->getTypedData()->getDataType()); // would not work (because it would lose its context)

I have not confirmed this fuzzy memory.

Edit #2: I'm still fine just making config an exception as suggested, but am curious what @e0ipso thinks if my memory is accurate

e0ipso’s picture

Although we can probably derive the desired information based on the config schema, I think that

[…] special case config entities and just make it output exactly what is stored

is a good compromise. Mainly because config entities are as close to normalized data as one can get (that's why we were using ->toArray() to normalize them). We need to make sure to not apply any further processing (like the IS proves).

With regards to #5 that is probably a good enhancement to ensure that ill-validated config entities stored in Drupal don't provide bad output. Maybe a follow up for this?

Thoughts?

PS: We should add this to the Core issue MVP if it's not already.

wim leers’s picture

@gabesullice: You may be right, but if those exist, they're definitely not very precise; they'd be super generic. Actually useful data definitions would only exist if they'd be generated based on the config schema.


@e0ipso

Mainly because config entities are as close to normalized data as one can get

Exactly.

With regards to #5 that is probably a good enhancement to ensure that ill-validated config entities stored in Drupal don't provide bad output. Maybe a follow up for this?

Stored config is NEVER allowed to be invalid. We don't need to do validation when normalizing. We do need to validate when denormalizing. And that doesn't exist yet…

e0ipso’s picture

We do need to validate when denormalizing. And that doesn't exist yet…

Well for Core inclusion we don't need to worry about that. We already agreed that we won't be supporting unsafe methods until validation is in place, right?

wim leers’s picture

Exactly :)

wim leers’s picture

Status: Active » Needs review
StatusFileSize
new22.58 KB

Let's first get some test coverage :) This should fail.

wim leers’s picture

StatusFileSize
new2.09 KB
new24.62 KB

I think that's still a little too naive and will break some lauded JSON API DX improvements. We want to check (1) the cardinality of the field and (2) whether its data definition is a sequence or a map.

A sequence w/ cardinality = 1 -> strip wrapper
A sequence w/ cardinality > 1 -> keep wrapper
A map                         -> keep wrapper

Actually, turns out this is not happening for config entities today anyway. In fact, because JSON API's field normalization architecture is centered on "content entity fields" and JSON API considers the data in config entities to also be "fields" (they aren't!), JSOn API's field normalization architecture is actually what's getting in the way!

For config entities, we don't need ANY post-processing!

In fact, the entire FieldNormalizerValue + FieldItemNormalizerValue value objects to be normalized are exactly what gets in the way for config entities! Because content entities have fields, and fields have items. Those items are keyed by deltas, which are always numerical. In other words: items are always a list with a certain sequence. IOW: the second level always has a numerical key.
But for config entities, JSON API treats their data as "fields", but it's possible that that second level has non-numerical keys. The FieldNormalizerValue + FieldItemNormalizerValue value objects do not allow this to be communicated, and this hence gets lost 100% of the time.

(I really tried to make it work and then ended up with that conclusion. I should've seen that right away.)

The only way to make it work is to modify \Drupal\jsonapi\Normalizer\Value\FieldItemNormalizerValue::rasterizeValue. But looking at \Drupal\Core\Entity\Entity, \Drupal\Core\Config\Entity\ConfigEntityInterface and \Drupal\Core\Config\Entity\ConfigEntityBase, there's zero mentions of "fields".

Therefore I think the best solution is to just massively simplify ConfigEntityNormalizer, and make it as dumb and simple as \Drupal\serialization\Normalizer\ConfigEntityNormalizer. Doing this also requires a new ConfigEntityNormalizerValue class, because the existing EntityNormalizerValue assumes that it contains FieldNormalizerValue objects.

So perhaps a better solution is to continue to map config entity's data onto "fields" after all, but to introduce a new \Drupal\jsonapi\Normalizer\Value\FieldNormalizerValueInterface implementation (to override FieldItemNormalizerValue::rasterizeValue() as mentioned before, and to also modify the constructor).

This approach turned out to be the easiest solution, but not the simplest. (Although it's definitely the simplest patch.) There's a lot of complexity and indirection wrt how config entities are normalized by JSON API … and most of that is arguably unnecessary and definitely unused. However, potentially rearchitecting that is out of scope for this issue.

The last submitted patch, 11: 2942979-11-tests_only_FAIL.patch, failed testing. View results
- codesniffer_fixes.patch Interdiff of automated coding standards fixes only.

Status: Needs review » Needs work

The last submitted patch, 12: 2942979-12.patch, failed testing. View results
- codesniffer_fixes.patch Interdiff of automated coding standards fixes only.

wim leers’s picture

Assigned: wim leers » Unassigned
Status: Needs work » Needs review
StatusFileSize
new1.93 KB
new25.23 KB

The ConfigEntityNormalizerTest unit test failed, but it's wrong… in fact, it was documenting exactly what the bug was! Fixed.

Also fixed all coding standards violations in the patch.

wim leers’s picture

+++ b/tests/src/Functional/ActionTest.php
@@ -84,13 +84,9 @@ class ActionTest extends ResourceTestBase {
-          // @todo Remove the first 3 lines in favor of the 5 commented lines in https://www.drupal.org/project/jsonapi/issues/2942979
-          // @codingStandardsIgnoreStart
-          'configuration' => 'anonymous',
-//          'configuration' => [
-//            'rid' => 'anonymous',
-//          ],
-          // @codingStandardsIgnoreEnd
+          'configuration' => [
+            'rid' => 'anonymous',
+          ],

+++ b/tests/src/Functional/EntityViewDisplayTest.php
@@ -91,15 +91,10 @@ class EntityViewDisplayTest extends ResourceTestBase {
           'content' => [
-            // @todo Remove the first 2 lines in favor of the 4 commented lines in https://www.drupal.org/project/jsonapi/issues/2942979
-            // @codingStandardsIgnoreStart
-            'region' => 'content',
-            'weight' => 100,
-//            'links' => [
-//              'region' => 'content',
-//              'weight' => 100,
-//            ],
-            // @codingStandardsIgnoreEnd
+            'links' => [
+              'region' => 'content',
+              'weight' => 100,
+            ],

By the way, this broke more than just dependencies.

It broke every case of a single-item array with a non-numerical key.

wim leers’s picture

#15 is green on both 8.4 and 8.6!

gabesullice’s picture

Status: Needs review » Reviewed & tested by the community

This looks great!

e0ipso’s picture

Status: Reviewed & tested by the community » Fixed

Boy! Thanks again for catching this one. Merging.

  • e0ipso committed 9a9ca33 on 8.x-1.x authored by Wim Leers
    Issue #2942979 by Wim Leers, e0ipso, gabesullice: JSON API's...
e0ipso’s picture

Edit: I posted in the wrong issue.

wim leers’s picture

Yay, glad to have this fixed, with test coverage!

Status: Fixed » Closed (fixed)

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