Problem/Motivation
First, a little bit of historical context
Even before shipping Drupal 8.0.0, and before the Drupal 8 ecosystem started adding functionality to Drupal core's REST/API-First support, it already was clear that the Symfony Serialization component that we use, is severely flawed in Drupal's plug-and-play model: #2575761: Discuss a better system for discovering and selecting normalizers.
When Drupal 8's REST module was at its height of initial development, in 2013, the HAL normalization seemed to be the future. 5 years later, it's at IETF spec draft (iteration 8), last updated in May 2016.
Besides HAL, we also support a "default" normalization, which is mostly the same, but without any hypermedia metadata.
Since then, JSON API has risen in popularity, reaching 1.0 in mid 2015 … i.e. after Drupal 8 was frozen, and only critical bugfixes could go in. A contrib module has been developed, and it's rising in popularity quickly: https://www.drupal.org/project/jsonapi. It offers a far better DX than core's REST module, because it solves many more problems (including collections, filtering, sorting, pagination, sparse fieldsets).
How does this relate to the issue title?
For many field types (@FieldType plugins), the normalizations of fields of those types at the time Drupal 8 shipped were either:
- incomplete, e.g. for
@FieldType=textfields, the processed text wasn't available. - not-so-great DX, e.g. for
@FieldType=timestampfields, a UNIX timestamp was returned, which led to bug reports about how to interpret them
We worked on addressing those problems. We followed the example of the existing normalizers, and hence ended up doing for example #2768651: Let TimestampItem (de)normalize to/from RFC3339 timestamps, not UNIX timestamps, for better DX (all others are still in progress). Problem solved!
Different normalizations structure their normalizations differently
This may sound obvious, but it is not.
- For the "default" normalization (provided by the
serializationmodule for thejsonandxmlformats), no top-level metadata keys are added the the normalization. - For the "HAL" normalization (
halmodule for thehal_jsonformat), top-level_linksand_embeddedkeys are added. - For the "JSON API" normalization (
jsonapimodule for theapi_jsonformat), everything is shifted two levels under adataand thenattributeskey, except for the UUID, which is relabeled toidand sits belowdata, next toattributes, and entity reference fields are shifted to arelationshipskey underdata, next toattributes.
As you can imagine, this requires that the field-level normalization is implemented differently.
Which finally brings us to the problem that this issue aims to solve: the normalizer added in #2768651: Let TimestampItem (de)normalize to/from RFC3339 timestamps, not UNIX timestamps, for better DX works for both the "default" normalization and the "HAL" normalization … because those happen to be similar enough. It doesn't work for the "JSON API" normalization. Which means that a contrib module developer would have to do the work:
- once for "default" + "HAL" (likely to happen because in core)
- again for "JSON API" (less likely to happen because in contrib)
- again for new contrib/custom normalizations (same)
Nobody had ever given this some thought. In fact, I even blamed the JSON API module for this at #2860350-14: Document why JSON API only supports @DataType-level normalizers.
Strengthening API-First Drupal
If we continue along the path we're on right now, contrib modules keep adding @FieldType-level normalizers with the problems described above. Which means that new/contrib normalizations are always going to be lagging very far behind core's normalizations. Which means some data will simply not be accessible.
Now, if we instead would implement @DataType-level normalizers (in our example: \Drupal\Core\TypedData\Plugin\DataType\Timestamp aka @DataType=timestamp instead of \Drupal\Core\Field\Plugin\Field\FieldType\TimestampItem aka @FieldType=timestamp), then the normalizations of non-core normalizations would automatically support those.
99% of all field items (@FieldType-level) behave the same, it's only their property values (@DataType-level) that really need normalizers. The primary exception to that rule is @FieldType=entity_reference, because it's what determines relationships/hyperlinks, and almost every normalization has its own way of dealing with those.
IOW:
- have format-specific high-level normalizers (at entity, field item list, field item levels)
- have generic (formatless)
@DataTypenormalizers (at property value level)
Proposed resolution
- Never implement
@FieldType-level normalizers, only implement@DataType-level normalizers. - Enforce this via test coverage: make the test discover all normalizer services, check the classes/interfaces it supports, those cannot be field types.
- Exempt the entity reference field type.
- This test should run for both core and contrib modules, which means some contrib modules' automated tests could start failing. This is necessary to nudge them to transition to
@DataType-level normalizers.
Remaining tasks
Write test that detects@FieldType-level normalizers and fails. This should fail in HEAD, because it contains a timestamp field normalizer.Fix #2926508: Add DateTimeNormalizer+TimestampNormalizer, deprecate TimestampItemNormalizer: @DataType-level normalizers are reusable by JSON:API, which should make the test pass.
See #62
User interface changes
None.
API changes
None.
Data model changes
None.
Release notes snippet
| Comment | File | Size | Author |
|---|---|---|---|
| #71 | 2926507-nr-bot.txt | 144 bytes | needs-review-queue-bot |
| #52 | 2926507-52.patch | 15.03 KB | wim leers |
| #52 | interdiff.txt | 4.57 KB | wim leers |
| #50 | 2926507-50.patch | 11.35 KB | wim leers |
| #50 | interdiff.txt | 1.35 KB | wim leers |
Comments
Comment #2
wim leersComment #3
wim leersFor context, this issue was created because I didn't even realize this problem. In fact, I blamed the JSON API module at #2860350-14: Document why JSON API only supports @DataType-level normalizers for being clearly buggy, because it wasn't picking up the improved normalization that #2768651: Let TimestampItem (de)normalize to/from RFC3339 timestamps, not UNIX timestamps, for better DX added.
@e0ipso explained at #2860350-21: Document why JSON API only supports @DataType-level normalizers why it worked this way, I didn't fully understand him at first (comment 23). Gradually, the understanding became clearer, and for the first time, I was grateful at the painfully slow rate of progress in fixing the normalizations of multiple field types … which I realized thanks to him should be fixed at the data type level instead. I summarized my understanding + the current status in #2860350-33: Document why JSON API only supports @DataType-level normalizers.
Comment #4
wim leersI'm happy to report that #2926508: Add DateTimeNormalizer+TimestampNormalizer, deprecate TimestampItemNormalizer: @DataType-level normalizers are reusable by JSON:API has proven to work. And in fact, it will be solving another issue at the same time: the normalization of
datetimeanddaterangefields (#2867206: API-First DX of "date" and "date range" fields: add 'datetime_iso8601' @DataType (de)normalizer using RFC3339 timestamps with timezones) … thanks to the fact that we'll be fixing this at the@DataTypelevel!I first moved the normalization logic from
\Drupal\Core\Field\Plugin\Field\FieldType\TimestampItemto\Drupal\Core\TypedData\Plugin\DataType\Timestamp, and then I realized (see #2926508-18: Add DateTimeNormalizer+TimestampNormalizer, deprecate TimestampItemNormalizer: @DataType-level normalizers are reusable by JSON:API) thatclass Timestamp extends IntegerData implements DateTimeInterface {, which means I might as well fix it a level up in the interface tree: at theDateTimeInterfacelevel.Doing that means that it also automatically supports
\Drupal\Core\TypedData\Plugin\DataType\DateTimeIso8601which isclass DateTimeIso8601 extends StringData implements DateTimeInterface. And by supporting that@DataType, thedatetimeanddaterangefields work automatically!Comment #5
wim leersI'll work on a patch that provides the test described in the issue summary later this week.
Comment #6
wim leersIt'd be great if Drupal 8.5 shipped with this, to discourage in code what I've already been actively been discouraging in issue comments and discussions.
I'll be working on a patch for this shortly.
Comment #7
wim leersWhile working on this, I learned something new about the Serialization module: there's a
NullNormalizerwhich allows one to blacklist arbitrary classes encountered during normalization!Also: I started by writing the change record: https://www.drupal.org/node/2934779 :)
Comment #8
wim leersComment #10
wim leers5 coding standards violations, telling us to create
usestatements. That'll be less clear, but sure, let's do that.And most importantly:
… probably because I got the namespace wrong 🤦♂️
Comment #11
wim leersGreen patch, hurray! Would be very very valuable to have this ship in 8.5.0 :)
Comment #12
gabesullices/ tothe/ to the/g
This feels like it's trying to win the "most compact if-statement award".
I am the biggest sinner when it comes to this, but a foreach would be much more clear here.
Comment #13
gabesullice$this->rootshould work insideKernelTestBaseComment #14
wim leers#12:
||. Just some more added in #2933991: Deprecation tests fail when all PHPUnit tests are run via PHPUnit. So … not sure what you're suggesting?#13: hah, great catch!
Comment #15
gabesullice#14.2 When these get a little too "compact" I try to break them apart into variables that are named according to what they "prove," like
$is_itemor$is_item_list. Then you can writereturn $is_item || $is_item_list;. However, I just tried to do that for this particular case and it really didn't add anything in the way of legibility so /shrug.LGTM now :)
Comment #16
wim leersOh, that makes sense! That's definitely a best practice, but here it'd likely add little.
Thanks for paying attention to details like that! 👍
Comment #17
larowlanSo the fact we've got to reference this here perhaps indicates that we might need a container parameter for this list - or some sort of API.
For example, entity reference revisions normalizer in contrib is also a special case that does lookup/resolution to set the revision ID.
Similary Entity Pilot provides an ER normalizer that works with unsaved entities in its 'preview' feature. It can do so because it has an alternate implementation of the entity resolver that can handle unsaved entities.
Can these be removed and replaced with DataType level normalizer? If not, how do they go about white-listing themselves?
And then if they do, how do we stop the floodgates?
Comment #18
wim leersGreat feedback!
<a href="http://cgit.drupalcode.org/entity_reference_revisions/tree/src/EntityReferenceRevisionsFieldItemList.php?h=8.x-1.x ">EntityReferenceRevisionsFieldItemListat minimum needs serious cleaning up:referencedEntities()is 95% identical, with several of the bugfixes in core not having been ported (for example #2404331: Can't serialise an entity with an entity reference to an unsaved entity (remove EntityReferenceItem::$NEW_MARKER in favour of a validation constraint) which you worked on!),processDefaultValue()is 100% identical. This is risky/dangerous.… and then I failed to do so in the actual patch!
Fixed :)
Comment #19
berdirYeah, also very unsure about this.
ERR is a good example, there's the current implementation but there are also issues to add more logic, for example to embed composite entities like paragraphs. That's IMHO logic that belongs on the field level and it is also format specific anyway as it basically recursively normalizes the entity using the HAL serialization, doubt that another format would just work.
We can document that when possible, it should be done in a way that works everywhere, but completely deprecating it? I'm not convinced that makes sense.
Another example is https://www.drupal.org/project/block_field/issues/2882727, how should that be done on a lower level, it's just a standard map property, we only want to act on block_field fields, not everything. (That is a workaround until core supports that natively, but still)
Comment #20
wim leersBased on this, I think you didn't read #18's interdiff, because that specifically addressed this?
As the #18 patch, the #18 interdiff, the #18.2 comment and the detailed rationale in the issue summary all state: yes, entity reference fields are a special snowflake, because they are what express relationships between different pieces of data stored in Drupal, and each format represents relationships differently. That's why we're adding this test to actively discourage adding more
@FieldType-level normalizers, except for entity reference fields!This is exactly an example of why solving things at a lower level results in things working for everything. That issue indeed initially set out to only change the normalization for
@FieldType= block_field. And the only thing that that patch (#2882727-8: Add custom HAL normalizer) is doing, is relying on the default normalization for all properties:followed by the only logic it really intends to add, to deal with
BlockFieldItem'ssettingsproperty, which is of the@DataType=map:As @JamesK at #2882727-13: Add custom HAL normalizer points out: there is no more need for the
@FieldType=block_fieldnormalizer once there is a@DataType=mapnormalizer, which is being added in #2895532: @DataType=map cannot be normalized, affects @FieldType=link, @FieldType=map.Comment #21
wim leersAlso, I see that the issue title is still more aggressive than it should be. That was the original issue title. The CR that I created a few days ago is more nuanced: https://www.drupal.org/node/2934779. Made the CR title also the issue title.
Comment #22
wim leersAlso, thanks @Berdir, for asking the critical questions :) 👌
Comment #23
wim leersDiscussed this with @Berdir in IRC:
Comment #24
dawehnerI'm wondering whether instead of hardcoding ER fields we could add something like an interface / annotation for you to opt into not warning about custom normalizers. I could imagine that pretty much every field type which potentially has more than one field would need some additional logic, not just on the data type level.
Comment #25
wim leersWhere you said "more than one field", you probably meant "more than one property"?
Comment #26
berdirThat was basically my argument as well. If a field type has custom logic (and not its data types), then it is possible that the normalization logic needs something custom too. Wim's argument was that the logic then should be on the data types and custom data types should be created but I'm not sure about that.
How can we possibly claim that EntityReferenceItem is indeed the only case that's special and nobody else can possibly come up with another valid use case?
And if we add an interface/annotation then we're not really deprecating anything anymore. Those plugins already say they want to work on the FieldType, now they need to add a @YesIReallyReallyWantTo too? :)
I think I still prefer having good documentation what should be done 80%/90% of the cases to support as many different formats and serializations as possible than forcing it.
Comment #27
wim leersFirst off: this issue is not about outright forbidding something. This is about nudging all contrib/custom developers in a direction where the normalizers they write work for and with the entire API-First ecosystem where possible:
@DataTypeor@FieldTypelevel, you should always prefer the@DataTypelevel if possible, because that means it'll work for the default normalization (json+xmlformats), the HAL normalization (hal_jsonformat) and the JSON API normalization (api_jsonformat) … because the generic field normalizer is enough, since in >90% of cases (I'd say >95%), your field really just contains N properties, and each of those properties needs to be normalized. Then@DataType-level normalizers are sufficient@DataTypelevel, then docs-generating modules like OpenAPI can be made aware of the shape of data, and have that work across all field types. If every field type has its own normalizer, it can't. If those@FieldType-level normalizers have perfectly matching normalization behavior, then OpenAPI would still need to know about all those field types (which really just reuse data types 95% of the time). But more likely, they don't have perfectly matching normalization behavior, in which case it's a complete nightmare.Sorry for repeating this again, but it feels necessary. It's about those things: nudging towards the choices that strengthen the API-First ecosystem. If we don't do this, we'll always have issues like #2931496: Add serializer for json/xml, where modules implemented one normalizer (in that case: HAL), but not others (default and JSON API), which leads to confusion and frustration for those using Drupal for decoupled use cases. (And yes, I know, that particular issue is an entity reference field, but you get the point.)
Comment #28
wim leersIt's the only case we currently know of. We can totally expand this to more in the future, if we find more. Let me quote the IS:
We're not deprecating any API, we're strongly discouraging. I think an interface actually can make sense, because that is an explicit signal for In fact, such an interface would even allow us to automatically surface to developers that a certain field type only has a normalizer for format X, but not formats Y and Z!
I'd be fine with that if it worked. But which developers read documentation thoroughly? And especially D8's REST docs, which have been historically weak (to put it nicely).
I am okay with postponing this issue and adding docs instead. But I think we really should explore a stronger signal: documentation is passive (the developer has to discover it), the
E_USER_DEPRECATEDerror I'm throwing in this test is active (the developer will be notified of it). Although not nearly actively enough, because they do need to run this test.So, that is the question: what do you think is the best way to reach developers of new normalizers in an active way, to give them guidance and nudge them in the direction that strengthens the Drupal API-First ecosystem?
Comment #30
damiankloip commentedI agree that it would be really nice if we could push people down this route of using property level normalizers where possible, but also share the same concern that berdir (and others) that there are potentially a lot of legitimate cases for field item level normalizers too. If we added an interface or annotation, I see the main problem being that a normalizer would be created at the field level, someone sees the warning in their logs, then does whatever is easy fix is to remedy this - which would be just adding whatever is needed to their normalizer. It is also a good point though, that seeing the notice will generate more awareness of the potential issues with using a normalizers at this level.
So I think we would certainly want good documentation to supplement this. The main concern I would have with the current patch is that being in the actual normalizer itself, this is more of a runtime check. This will generate a lot of errors in the logs. Could we instead maybe do a requirements check for the status page instead? That way we wouldn't be generating a ton of duplicate errors. I think an interface or something could work in conjunction with this still.
Comment #31
wim leersThat won't trigger deprecation errors, meaning developers won't see it. Status report is meant to be actionable, especially for the site builder/owner. This would not be actionable. This is why I started with pure test coverage, but ended up also doing a runtime check, because otherwise only core developers would ever see the failing test.
Given that, it sounds like we're shifting towards a "documentation-only" approach. I can live with that. It's not ideal, but neither is the current patch.
Everyone +1 to just documenting this in
serialization.api.phpandrest.api.php?Comment #32
dawehnerI wonder whether there is a weaker form of errors than deprecation errors. Basically you know a way for us to log and gather data so people tell us: We are using field level normalizers. Based upon that we could do some educated decision one day.
Comment #33
wim leers@dawehner: so basically … more data than what https://www.drupal.org/project/usage/drupal (and hence the
update.modulemodule in core) is giving us? That'd be nice, but collecting more data is … very sensitive. I agree that'd be very nice though!Comment #34
dawehnerYeah, somehow something which reports extended class methods etc., but sure, it would be an opt in tool, but it could be dramatically useful.
Comment #35
berdirOne additional thing that I just figured out is that this only works for *normalizing*.. it's not possible to do denormalization on the property level because \Drupal\serialization\Normalizer\FieldItemNormalizer() does that itself.
One more argument why this for now doesn't make much sense IMHO.
Comment #36
wim leersThis is a VERY good point, and in fact a very good find! One more way in which the REST API was severely flawed in the way it shipped 🤐😞 Once again relying on magic rather than explicitness, and the Serialization module not having sufficient test coverage — and still not to this day obviously! Fortunately, it's fixable. In theory at least.
Also, essentially the same bug was discovered in JSON API only days ago #2955615: Field properties are not being denormalized …
I agree that until that is fixed, this does not make sense.
Created #2957385: FieldItemNormalizer never calls @DataType-level normalizer service' ::denormalize() method.
Comment #37
wim leersFYI: independently/simultaneously with #35/#36, the same problem was discovered in JSON API. See #2955615: Field properties are not being denormalized. That now has a green patch! The sister issue for core (#2957385: FieldItemNormalizer never calls @DataType-level normalizer service' ::denormalize() method, created in #36) should be able to use the same approach. Once that lands, this issue will be unblocked again.
Comment #39
jibranThis means DER needs to remove https://cgit.drupalcode.org/dynamic_entity_reference/tree/src/Normalizer... and add a normalizer for https://cgit.drupalcode.org/dynamic_entity_reference/tree/src/Plugin/Dat...?
Comment #40
wim leers#39: Correct, that's where we want to be going.
Comment #41
wim leersIn response to @Berdir's excellent remark in #35, #2957385: FieldItemNormalizer never calls @DataType-level normalizer service' ::denormalize() method was opened, and its patch is now passing tests!
Comment #42
wim leers#2957385: FieldItemNormalizer never calls @DataType-level normalizer service' ::denormalize() method landed weeks ago.
#2926508: Add DateTimeNormalizer+TimestampNormalizer, deprecate TimestampItemNormalizer: @DataType-level normalizers are reusable by JSON:API landed yesterday.
This can now move forward again.
Comment #43
wim leersRebased #18.
Comment #45
wim leersCool, that failure proves it works: it is failing due to the deprecation errors being added here :)
Comment #47
wim leersFinally following up on #45…
Comment #48
wim leers"is being" → this was completed more than 6 months ago.
Comment #50
wim leersComment #52
wim leersComment #53
gabesulliceI think this looks good, it should be RTBC if tests pass IMO.
Comment #54
gabesulliceComment #55
jibranCan we update change notice to mention how can a contrib module opt-in? If we don't want to encourage it then at least document it here because DER needs to opt-in as per #39.
Comment #56
wim leersI'm not sure what "opt-in" you are referring to?
Comment #57
jibranSorry, ignore that. This will let DER/ERR Normalizer skip the warning.
Comment #58
larowlanI have only one question here -
So playing devil's advocate here, let's say in 2020, a new format becomes the rage and a contrib module provides the normalizers for field and field items for it. How does it say 'I'm allowed'. Should we have an interface or a settings option/api here? I realise doing so might result in contrib modules abusing it for things that should be DataTypeLevel.
Comment #59
jibranI'd say let's create a follow-up for this and figure out a long term plan there.
Comment #60
berdirFWIW, I still think this is highly problematic and I'm basically against it.
If everything in disallowFieldLevelNormalizers() would be deprecated in core (beside the generic ones that pass things through), then I'd see the point, but they are not. Especially entity reference normalizers are still very much not deprecated with AFAIK no replacement in sight?
And if a contrib module wants to implements its own format, then it *must* provide field-level normalizers to pass things through?
Comment #61
wim leers#58: see #3014283-36: Use ResourceTypeFields to statically determine the field normalizer + #3014283-37: Use ResourceTypeFields to statically determine the field normalizer + #3014283-38: Use ResourceTypeFields to statically determine the field normalizer: the JSON:API module will probably move away from using the Symfony serialization component altogether for anything that isn't at the
@DataType-level.Comment #62
berdirAs an example, I see \Drupal\metatag\Normalizer\MetatagNormalizer locally.
And based on "http://grep.xnddx.ru/search?text=extends NormalizerBase", some possibly interesting projects, didn't really investigate much, but wotapi/webapp_json for example seem to imlement their own formats, so how else are they supposed to do this? The patch explicitly whitelists hal + jsonapi, how are they special just because they are in core?
https://www.drupal.org/project/wotapi
https://www.drupal.org/project/value
https://www.drupal.org/project/webapp_json
Also, the issue title/description uses "Discourage" and "nudge" but then what we do is a E_USER_DEPRECATION, without actually using the deprecation format. That seems pretty confusing. So contrib tests will fail on that, just like actual D9 deprecations?
Comment #63
larowlanFor #62
Comment #71
needs-review-queue-bot commentedThe Needs Review Queue Bot tested this issue. It either no longer applies to Drupal core, or fails the Drupal core commit checks. Therefore, this issue status is now "Needs work".
Apart from a re-roll or rebase, this issue may need more work to address feedback in the issue or MR comments. To progress an issue, incorporate this feedback as part of the process of updating the issue. This helps other contributors to know what is outstanding.
Consult the Drupal Contributor Guide to find step-by-step guides for working with issues.
Comment #73
quietone commentedSee #63
Comment #74
smustgrave commentedNot sure what the policy was then but shouldn't this be using deprecation now?
Comment #75
smustgrave commentedShould this go to PNMI if it's still needed?
Comment #76
smustgrave commentedBelieve this needs to use proper deprecation.
Question is do we still want to deprecate in D10?
Comment #77
quietone commentedSetting to NW so that the remaining tasks and items in the tags can be resolved.