jsonapi.api.php currently says:
The JSON:API module provides *no PHP API to modify its behavior.*...This means that this module's implementation details are entirely free to change at any time.
And correspondingly, every class and interface are marked @internal.
However, I don't think that's tenable. We know there are use-cases for wanting to customize something. For example, renaming a field from its internal Drupal name to something friendlier (e.g., "uid" to "author"). That is currently supported by ResourceType::getPublicName(). I think that's an example of something we should expose to a non-internal API (whether by creating a ResourceTypeInterface, or via some other way).
Let's use this issue to collect other examples and spin-off child issues as needed.
Public PHP APIs shipped
- Drupal 8.8: #3037039: Create a public API for indicating resource types should not be exposed
- Drupal 8.8: #3085035: Add a public API for aliasing and disabling JSON:API resource type fields
- Drupal 9.3: #3105318: Add a public API for aliasing resource type names
- Drupal 10.4 & 11.1: #3110831: Method to enable a resource type field disabled by a previous ResourceTypeBuildEvent subscriber
- Drupal 11.2: #3100732: Allow specifying metadata on JSON:API objects
Public PHP APIs proposed
- #3104408: Allow to include a total count of items to collection responses
- #3026432: Increase max pager size, ideally per resource type
- #3233410: Add methods to set locatable, mutable flags in ResourceTypeBuildEvent
- #3079254: API for JSON:API specific "extra" fields, e.g. for entity labels
Comments
Comment #2
wim leersThanks for creating this issue. Rather than doing what Drupal has traditionally done and provide APIs for all the things, the JSON:API module takes the approach that many other software platforms take: keep APIs private and unsupported (
@internalin Drupal parlance) until they've been sufficiently validated by developers building on top of it.I'm relieved we did it this way, otherwise the JSON:API module would not have been in nearly as good shape. It has evolved a lot in the last 18 months.
So, let's wait and see which public APIs contrib module developers request. The sequence:
@internalAPI — iterate based on feedback@internal)Comment #3
effulgentsia commentedYep, I mostly agree with #2. However,
I would split this into 2 parts:
For anyone following this issue who already knows of certain needs (from experience with JSON:API Extras or other modules), please add comments about them, so we can start capturing them and beginning this process. Or, if these are already written down somewhere in various issues, then please link to them. Thanks!
Comment #4
wim leersThat's fair :) I meant #2 to be the rule of thumb, not as an iron-clad process. So 👍
P.S.: for the particular use case that you're citing — field aliasing — I'm actually not convinced that we need a public API for that. I think we could and should solve this at the Typed Data level, much like https://www.drupal.org/node/2916592. That would then could then also serve as the facility for Drupal entity types to rename fields while retaining BC.
Comment #5
effulgentsia commentedAdded #3037039: Create a public API for indicating resource types should not be exposed as a child issue.
Comment #6
gabesulliceThe JSON:API Comment module exposes routes that enhance the experience of using JSON:API with comments.
It is forced to use several of JSON:API's internals to accomplish this. Primarily, the
LinkandLinkCollectionclasses and several methods inEntityResource.php.WRT to links, I think #3014704: Expose API to allow links handling for entities from other modules describes the need (at least its title does). There's an example of how we might accomplish that in the JSON:API Hypermedia module. That module decorates the link collection normalizer and then passes the link collection to be normalized into a "LinkProvider" which is able to add/remove/update any links. This pattern worked well and I think could be imported into core JSON:API.
The primary shortcoming I found was links is that it is difficult to handle access + cacheability.
Drupal\Core\Urlhas anaccessmethod which I think is meant to be used to hide links when the target resource is inaccessible, but it doesn't return cacheability information. IOW, it's hard to associate cacheability information with the response if a link is not in the response. I think that theLinkclass could gain a new argument to its constructor which would take anAccessResult. The Link would then be omitted if access is forbidden but its cacheability could still be added to the response.As for
EntityResource, the JSON:API Comment module extends it for two purposes:POSTrequests and add some default field dataThe custom collection is fairly standalone. Its primary dependencies are on the classes in the
JsonApiResourcenamespace. I found no shortcomings with the API for those objects. After that, it usedEntityResource::getIncludesandEntityResource::buildWrappedResponse. I think that shows that theIncludeResolverwill likely need to be public. OTOH,buildWrappedResponsedoesn't provide much value. I just used it because I could. I don't think it needs to be a public API. Finally, there was a dependency onEntityAccessChecker::getAccessCheckedResourceObject; again, I found no real shortcoming there.Accepting POST requests is a different story. My implementation is very tightly coupled to
EntityResourcebecause I was forced to copy the contents ofcreateIndividualto do deserialize, check access, validate and return a compliant response. This was only necessary because I needed to add some default field values to the parsed entity. If I had a more granular way to do that, my code could have been much cleaner.Finally, what I discovered that is not closely related to concrete code is that custom JSON:API resource may not be able to support the
sort,filterandpagequery parameters because those are closely coupled to an entity query and your custom collection may not necessarily be backed by something that can be made into an entity query. If we provide an API for custom collections, we should make sure that those come à la carte. OTOH, it seems that thefieldsandincludeparameters may be broadly applicable.Comment #7
gabesulliceI created #3055889: JsonApiResource\Link objects with inaccessible target urls should not be normalized based on #6.
Comment #8
wim leers#6: Exciting! :) Thanks for sharing your insights!
Indeed. Instead of working around this limitation, let's fix it? This is now a core module, so we can fix core :) This can use the same backwards-compatible pattern as
\Drupal\Core\Access\AccessibleInterface::access().Can't we fix this in core too? i.e. at the Entity/Field/Typed Data level?
Comment #9
gabesulliceI think we can/should do that. I don't know if it would completely solve the problem though. I think that only checks the access requirements on a route. At present, we have certain access controls run in controllers. Also, consider this link which I added as part of my recent project:
Route access would only indicate whether the user has
updatepermission for the comment, which could be true even if the user does not have theadminister commentspermission. So, you'd want to hide this link based on more specific knowledge that a route access checker could provide.I'm not sure TBH. It's a weird one. The field values need to be set prior to entity validation and they depend on the current route. In my case, I'd only want those default values to be set on a certain route, not for all entity save operations. I think what would be more effective in this case might be pre and post-(de)normalization hooks of some kind.
Comment #10
effulgentsia commentedYes, for something like
unpublish, you'd want to merge the access check for the comment entity and for the status field.That's something we might want to fix, but how is it relevant to this issue? Why would we want to invoke
Url::access()in the case where we know we're operating on an entity and so can invoke the entity's access methods? Are there other cases you have in mind where JSON:API needs to perform access on something that isn't an entity?Comment #11
gabesulliceTBC, I actually don't think we want to rely on
Url::access. I brought it up because aUrlobject is the second argument toLink. I was trying to anticipate the question, "why isn'tUrl::accesstotally sufficient and why can't we just make that better?" The answer is: "because route access alone is not enough, you may want to run more granular/extra access checks". I guess I didn't do a very good job at communicating that.When I said: "I think that the
Linkclass could gain a new argument to its constructor which would take anAccessResult. I was imagining an API that would be used kind of like this:@effulgentsia, I think that's probably pretty similar to where your head was at. You can see that it's running entity/field access checks, but it's also running an extra check on the field value itself. That's there to drive the client-side application state a bit. IOW, we only show a link that makes sense at the given moment, not just whether its strictly allowed or not.
Comment #12
grimreaperHello,
Thanks for opening this issue.
As the maintainer of Entity share https://www.drupal.org/project/entity_share, here are my needs:
I have a service (https://git.drupalcode.org/project/entity_share/blob/8.x-2.x/modules/ent...), entity_share_client.jsonapi_helper (which will need to be splitted on the future...) which needs 3 internal services from JSON:API:
jsonapi.serializer is only used to be passed to serializer.normalizer.jsonapi_document_toplevel.jsonapi.
serializer.normalizer.jsonapi_document_toplevel.jsonapi is used to get an entity from JSON data.
jsonapi.resource_type.repository is used for serializer.normalizer.jsonapi_document_toplevel.jsonapi and to handle field alias (getInternalName() and getPublicName()). I also think to use it for the hasField() method in the future to check data structure.
Last year, there has been a discussion to be able to not rely on those services #2939827: Provide a supported API for entity denormalization and a possible solution #2939827-19: Provide a supported API for entity denormalization would be to use
\Symfony\Component\HttpKernel\HttpKernelInterface::SUB_REQUEST.I only tried one time to use this solution, unfortunately since the beginning of 2018, I am barely assigned time to Entity share, I hope to have more time (and more important, in a regular way) stating from now.
Now that there are more modules in the JSON:API ecosystem (https://www.drupal.org/project/jsonapi/ecosystem) and also https://www.drupal.org/project/jsonapi_operations. Maybe I will find more examples.
Also, initialy the issue #2939827: Provide a supported API for entity denormalization had been created in the JSON:API issue queue. Should it be back there?
Thank you all for your work on Drupal and JSON:API!
Hope to see you at DrupalDevDays 2019!
Comment #13
e0ipsoMoving to Drupal core's issue queue.
Comment #14
wim leers#3037039: Create a public API for indicating resource types should not be exposed just got committed and is the first public PHP API in the
jsonapimodule! 🎉The second one is already on its way: #3085035: Add a public API for aliasing and disabling JSON:API resource type fields builds on that first one and is also RTBC 😀
Comment #16
dmitry.kazberovichHi
One more use case is sorting comments by thread.
1. With JSONAPI we can get comments of exact node and sort them by thread:
/jsonapi/comment/comment?filter[by_node][condition][path]=entity_id.id&filter[by_node][condition][value]=node_uuid_here&sort=thread2. But the result is not we want to see, because the "ORDER BY" is a little bit more complicated: https://github.com/drupal/core/blob/8.8.x/modules/comment/src/CommentSto...
3. Comments module provides custom sorting for views: https://github.com/drupal/core/blob/8.8.x/modules/comment/src/Plugin/vie...
4. It would be nice to have the same approach for JSONAPI: customize sorting by defining the plugin in 3rd party module
Comment #17
gabesulliceHave you seen the JSON:API Comment module yet @dmitry.kazberovich? That module creates new JSON:API endpoints to fetching, authoring and replying to comments. It also takes care of thread sorting. Additionally, the Fluid Comment module contains a React app that supports comment threading based on that former module if you need examples.
Comment #18
wim leers#3085035: Add a public API for aliasing and disabling JSON:API resource type fields was also completed! 🥳
New: #3100732: Allow specifying metadata on JSON:API objects.
Comment #19
dwwI know the component is "jsonapi.module", but I'm giving this a more self-documenting title for when it shows up in issue listings across core or per-user. ;)
Cheers,
-Derek
Comment #20
wim leers👍 Thanks!
Comment #21
bojanz commentedI would like to see a way for entity types to specify the resource type ID in their annotations.
Take the commerce_product entity type. None of our URLs include "commerce_product", they're all "product/". I would expect to be able to have the same control over JSON API links, so that I can specify "products" and have the URL be "jsonapi/products/clothing", etc.
This is currently doable by decorating the resource type repository and providing a custom resource type object (with an overridden getPath() method), but that is a bit verbose for what feels like a common need.
Comment #22
wim leers#21: created an issue for that: #3105318: Add a public API for aliasing resource type names.
Comment #26
bbralaProbably another one for in this list: #3104408: Allow to include a total count of items to collection responses
Comment #27
bbralaComment #28
bbralaAdded documentation issue for
ResourceTypeBuildEventsince that has not been done in any of the issues yet.Comment #29
bbralaMoving issue to shipped since it has been committed for 9.3
Comment #30
wim leersAdding #3026432: Increase max pager size, ideally per resource type after moving it from the contrib
jsonapiproject from ~2.5 years ago into the Drupal core issue queue.Comment #31
wim leersIndicating which Drupal minor each public PHP API shipped in 😊
Comment #32
bbralaComment #33
bradjones1Comment #34
bradjones1Comment #37
bbralaComment #39
wim leersComment #41
wim leersComment #42
jonathan_hunt commentedFurther to #3100732: Allow specifying metadata on JSON:API objects, there doesn't seem to be a method to inject a
metamember (https://jsonapi.org/format/#document-meta) at the top level of an API response. Should there be a new issue to address that? My use case is to expose overall content licence and site api version data, independent of the jsonapi version.Comment #43
bbralaYeah that should be a new issue.
Comment #44
lind101 commentedNot sure if this is the right place for this (please point me in the right direction if there is a more useful place), but just thought I'd drop in a few things that I'm struggling with while developing an API over the last few weeks. These mainly revolving around the extensibility of core JSON:API services due to method access modifiers (predominantly the EntityResource controller class).
What lead me here is I wanted to try and re-use the core code in
EntityResource::getJsonApiParams()andEntityResource::getCollectionQuery()in a Custom Resource by calling these methods, but couldn't because they have protected access, I then went down a rabbit hole...For context I'm creating a few Custom endpoints using the JSON:API Resource module with JSON:API Extras installed to enable me to easily alias resources and provide defaults, and JSON:API Include allowing me to provide a cleaner response for the consumer in certain scenarios. Quite the module cocktail!
All of these modules are great and have worked OOB. However a lot of the time they have needed to modify a core JSON:API service, add a "shim" service or create a new service that extend an existing one etc. The main reason for this is to allow them to extend protected methods on the base service. Trying to build on-top of this very tricky when developing a module to extend the core features as you now have to be aware that popular modules (I imagine JSON:API Extras is pretty much a standard) are rolling their own instances of core services so you cannot reliably extend them yourself.
For example jsonapi_defaults part of JSON:API Extras replaces the class used for the
jsonapi.entity_resourceallowing them to add default parameters in the protectedgetJsonApiParamsmethod. Had thegetJsonApiParamsbeen public the same could have been achieved with a service decorator, which would then allow other modules to add decorators as well without having to know what other modules are trying to extend these services. As it stands, if I want to extend some functionality to `getJsonApiParams` and retain the functionality provided by jsonapi_defaults, I have to include the jsonapi_defaults module as a dependency and extend that class.As the module ecosystem around JSON:API grows, this is going to a bit of a blocker. Apologies if I don't have the back-story to the method access on these services (I'm sure there is a good reason)!
My two cents (for what it's worth) on things that might help the DX around extending JSON:API:
$params = \Drupal::service(Routes::CONTROLLER_SERVICE_NAME)->getJsonApiParams(Request $request, $resource_type);$query = \Drupal::service(Routes::CONTROLLER_SERVICE_NAME)->getCollectionQuery($resource_type, $params, $cacheability);Hopefully that's a useful view of working with the module and it's ecosystem (which is ace btw) and might help a bit with moving it forward. For now I'll build my new features specifically for the application in question as I know what the dependency tree is and can extend the relevant classes.
Cheers! 🤟
Comment #45
wim leers#3100732: Allow specifying metadata on JSON:API objects is in!