Support list resources not sourced from entities
>>> [!note] Migrated issue <!-- Drupal.org comment --> <!-- Migrated from issue #3275484. --> Reported by: [guypaddock](https://www.drupal.org/user/156932) Related to !1 >>> <h3 id="summary-problem-motivation">Problem/Motivation</h3> <p>When writing a custom JSON:API resource like the one in <code>\Drupal\jsonapi_resources_test\Resource\CurrentUserInfo</code>, it is extremely difficult to expose fields as relationships (e.g., the <code>roles</code> field in that example) unless they originate from an entity field that implements the <code>\Drupal\Core\Field\FieldItemListInterface</code> interface. This is the result of several design limitations/assumptions in the Core implementation of JSON:API that I think JSON:API Resources could provide a workaround for.</p> <!--break--><p> I was able to demonstrate in <span class="drupalorg-gitlab-issue-link project-issue-status-info project-issue-status-7"><a href="https://www.drupal.org/project/jsonapi_extras/issues/3275196" title="Status: Closed (fixed)">#3275196: Skip Custom JSON:API Resource Types that Are Not Configurable</a></span> (a bug report filed against JSON:API Extras) that at least for the <code>\Drupal\jsonapi_resources_test\Resource\CurrentUserInfo</code> example resource, it <em>is</em> possible to expose the roles as a real relationship in a response, complete with <code>includes</code> support, but this only works because the role information can be fetched directly from the <code>UserInterface</code> entity as a <code>FieldItemListInterface</code>. This doesn't work for our project because we have custom resources that aren't backed by any entity in the system; our resources are either programmatically generated or are based on information sourced from another internal web service that gets combined with information from Drupal. In our case, it does not make sense for us to store this data as an entity since the data is subject to frequent change.</p> <h4 id="summary-steps-reproduce">Steps to reproduce</h4> <p>Try to write a custom resource that exposes programmatically-generated data as a "relationship" field. If your data is not wrapped in an entity reference field from a real entity in the system, you will encounter one of the following problems:</p> <ol> <li>You will get an assertion failure in <code>\Drupal\jsonapi\Normalizer\Value\CacheableNormalization::__construct()</code> because <code>\Drupal\jsonapi\Normalizer\ResourceObjectNormalizer::serializeField()</code> assumes that any field that does not implement <code>\Drupal\Core\Field\FieldItemListInterface</code> must not need to be normalized, even if the field is pointing at a <code>\Drupal\jsonapi\JsonApiResource\Relationship</code> or <code>\Drupal\jsonapi\JsonApiResource\ResourceObject</code>. So, it wraps the value in a call to <code>CacheableNormalization::permanent()</code>, resulting in the error that a normalization is being wrapped in another normalization.</li> <li>If you patch <code>\Drupal\jsonapi\Normalizer\ResourceObjectNormalizer::serializeField()</code> with code like the following:<br> <div class="codeblock"> <pre><span style="color: #000000"><span style="color: #0000BB">&lt;?php<br></span><span style="color: #007700">class </span><span style="color: #0000BB">ResourceObjectNormalizer </span><span style="color: #007700">extends </span><span style="color: #0000BB">NormalizerBase </span><span style="color: #007700">{<br>&nbsp; </span><span style="color: #FF8000">// ... lines omitted ...<br><br>&nbsp; </span><span style="color: #007700">protected function </span><span style="color: #0000BB">serializeField</span><span style="color: #007700">(</span><span style="color: #0000BB">$field</span><span style="color: #007700">, array </span><span style="color: #0000BB">$context</span><span style="color: #007700">, </span><span style="color: #0000BB">$format</span><span style="color: #007700">) {<br>&nbsp;&nbsp;&nbsp; </span><span style="color: #FF8000">// ... lines omitted ...<br>&nbsp;&nbsp;&nbsp; </span><span style="color: #007700">if (</span><span style="color: #0000BB">$field </span><span style="color: #007700">instanceof </span><span style="color: #0000BB">FieldItemListInterface</span><span style="color: #007700">) {<br>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; </span><span style="color: #FF8000">// ... lines omitted ...<br>&nbsp;&nbsp;&nbsp; </span><span style="color: #007700">}<br>&nbsp;&nbsp;&nbsp; elseif (</span><span style="color: #0000BB">$field </span><span style="color: #007700">instanceof </span><span style="color: #0000BB">Relationship</span><span style="color: #007700">) {<br>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; </span><span style="color: #0000BB">$normalized_field </span><span style="color: #007700">= </span><span style="color: #0000BB">$this</span><span style="color: #007700">-&gt;</span><span style="color: #0000BB">serializer</span><span style="color: #007700">-&gt;</span><span style="color: #0000BB">normalize</span><span style="color: #007700">(</span><span style="color: #0000BB">$field</span><span style="color: #007700">, </span><span style="color: #0000BB">$format</span><span style="color: #007700">, </span><span style="color: #0000BB">$context</span><span style="color: #007700">);<br>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; </span><span style="color: #0000BB">assert</span><span style="color: #007700">(</span><span style="color: #0000BB">$normalized_field </span><span style="color: #007700">instanceof </span><span style="color: #0000BB">CacheableNormalization</span><span style="color: #007700">);<br>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; return </span><span style="color: #0000BB">$normalized_field</span><span style="color: #007700">;<br>&nbsp;&nbsp;&nbsp; }<br>&nbsp;&nbsp;&nbsp; else {<br>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; </span><span style="color: #FF8000">// ... lines omitted ...<br>&nbsp;&nbsp;&nbsp; </span><span style="color: #007700">}<br>&nbsp; }<br>}<br></span><span style="color: #0000BB">?&gt;</span></span></pre></div> <p>And you populate your field with <code>\Drupal\jsonapi\JsonApiResource\Relationship</code> instances (i.e., you sub-class <code>Relationship</code> to add a static constructor that can create a relationship from your custom data), you can get the relationship data to appear in the response. But, if you attempt to include the referenced data using <code>include</code> in the query string, the data won't be included. This is because <code>\Drupal\jsonapi\IncludeResolver::resolveIncludeTree()</code> bails out on including anything that isn't coming from a <code>\Drupal\Core\Field\FieldItemListInterface</code>.</p></li> </ol> <h3 id="summary-proposed-resolution">Proposed resolution</h3> <ul> <li>Write a decorator that wraps <code>\Drupal\jsonapi\Normalizer\ResourceObjectNormalizer</code> to extend it to support relationships source from other types of fields than just <code>\Drupal\Core\Field\FieldItemListInterface</code> and <code>\Drupal\Core\Field\EntityReferenceFieldItemListInterface</code>. JSON:API has protections against custom normalizers, but those protections can be defeated using namespace trickery like that used by <a href="https://www.drupal.org/project/jsonapi_extras">JSON:API Extras</a> (as described in <span class="drupalorg-gitlab-issue-link project-issue-status-info project-issue-status-7"><a href="https://www.drupal.org/project/jsonapi_extras/issues/3036904" title="Status: Closed (fixed)">#3036904: Decorate JSON:API's normalizers and fake its namespace so JSON:API can eliminate its Serializer's fallback behavior.</a></span>).</li> <li>Write a decorator that wraps <code>\Drupal\jsonapi\IncludeResolver</code> to modify the logic of <code>\Drupal\jsonapi\IncludeResolver::resolveIncludeTree</code> so that it can handle includes of custom resources.</li> </ul> <h3 id="summary-remaining-tasks">Remaining tasks</h3> <h3 id="summary-ui-changes">User interface changes</h3> <h3 id="summary-api-changes">API changes</h3> <h3 id="summary-data-model-changes">Data model changes</h3> > Related issue: [Issue #3125777](https://www.drupal.org/node/3125777)
issue