plone.jsonapi.core
An extensible Plone JSON API framework.
- Author: Ramon Bartl
- Version: 0.8.0
Abstract
plone.jsonapi.core lets you expose content and functionality of a Plone
site as JSON, by registering routes that are dispatched to endpoint
functions.
Features
- A Werkzeug-based router that dispatches
@@APIrequests to endpoint functions orIRouteProviderutilities. - All standard HTTP methods (
GET,POST,PUT,PATCH,DELETE) reach the API view; the framework clears Zope's WebDAV handling for API requests so the non-GET/POST verbs are routed instead of being intercepted. - A typed exception hierarchy (
APIErrorand subclasses such asNotFoundError,UnauthorizedError,ForbiddenError,ValidationError) mapped to the correct HTTP status codes. - A swappable error handler (
IErrorHandlerutility) that renders a consistent JSON error envelope without leaking tracebacks. - Proper
404/405responses for unknown routes and methods. - Opt-in CORS support (
add_cors_headers/ the@corsdecorator). - A thread-safe router: per-request host/scheme are read from the current request, never cached on the shared router.
Architecture and scope
plone.jsonapi.core is intentionally a thin routing layer, not a
full REST framework. Understand what it does and — just as importantly
— what it deliberately leaves to you:
-
It bypasses object traversal. The
@@APIbrowser view swallows the whole sub-path and hands it to a URL router. Routes therefore have stable, flat URLs that are independent of where content lives in the site, and an endpoint is free to answer straight from the catalog without waking up content objects — which is fast, but means the request never acquires a content object's security context. -
It does not serialize for you. An endpoint returns a plain
dict(or list); turning content into that structure is the endpoint's job. There is no serializer/deserializer adapter layer. -
It does not enforce permissions. This is the most important thing to know. The
@@APIview is published with thezope2.Viewpermission, which Anonymous holds at the site root, and because the request bypasses traversal there is no per-object security check. Every route implementation is responsible for authorizing the request itself (see Permissions). A route that forgets to check a permission is reachable by anonymous callers. Treat "check the permission first" as the default for every non-public endpoint.
If you need content-negotiated serialization, HATEOAS, or
traversal-based per-object security out of the box, plone.restapi is
the heavier but batteries-included alternative. plone.jsonapi.core is
for when you want a small, explicit router and full control over each
endpoint.
Motivation
This project was born in 2012, out of the need for a data source to build a network-based iOS application — or more precisely, wanting to learn iOS programming and knit a JSON API for it.
Providing a routing mechanism for Plone that dispatches the request
after the ZPublisher has done its job is a little unusual, but it
worked, and so it stuck.
HTTP methods
All standard HTTP methods reach the API view: GET, POST, PUT,
PATCH and DELETE. Earlier versions were limited to GET and POST
because Zope's publisher diverted the other verbs to its WebDAV
machinery before they reached the @@API view; the framework now
clears that handling for API requests so every verb is routed normally.
Because the API view is published with the zope2.View permission, you
must programmatically check for the correct permissions on your custom
routes (see Permissions).
Compatibility
plone.jsonapi.core works with Plone 5.2 on
Python 2.7 and Python 3.8.
Installation
The official release is on PyPI, so you simply include
plone.jsonapi.core in your buildout config:
[buildout]
...
[instance]
...
eggs =
...
plone.jsonapi.core
API URL
After installation, the API view is available as a browser view on your
Plone site with the name @@API, for example
http://localhost:8080/Plone/@@API.
API framework
The main work is done in the plone.jsonapi.core.browser.api module.
It dispatches the incoming request to an endpoint function.
The API router
The Router manages and maintains API routes to endpoints.
Routes are defined by so-called "route providers". A route provider is
either a named utility that implements the IRouteProvider interface,
or simply a function registered via the add_route decorator.
Basic example
The most basic route provider is a decorated function:
from plone.jsonapi.core import router
@router.add_route("/hello/<string:name>", "hello", methods=["GET"])
def hello(context, request, name="world"):
return {"hello": name}
The context and request are passed in from the @@API view and can
be used to query Plone tools, utilities or adapters.
A more complex example
Here we add a route provider named my_routes, registered as a named
utility. Add a routes.py module to your package:
from zope.interface import implementer
from plone.jsonapi.core.interfaces import IRouteProvider
@implementer(IRouteProvider)
class ExampleRoutes(object):
def initialize(self, context, request):
"""Called by the JSON API framework."""
pass
@property
def routes(self):
return (
("/hello/<string:name>", "hello", self.json_hello,
dict(methods=["GET"])),
)
def json_hello(self, context, request, name="world"):
return {"hello": name}
Register the utility in your configure.zcml:
<!-- Extension point for custom routes -->
<utility
name="my_routes"
provides="plone.jsonapi.core.interfaces.IRouteProvider"
factory=".routes.ExampleRoutes" />
Each route provider is initialized with the context and the request
in an initialize method called by the API framework. The provider
must expose a routes property (or method) returning a tuple of route
definitions. Each definition is the URL rule (/hello/<string:name>),
an endpoint name (hello), the callable to invoke on a match
(self.json_hello), and a dictionary of routing options.
The options dictionary is passed straight to the
Werkzeug routing machinery; see
its rule format.
plone.jsonapi.coreships a default router built on Werkzeug. You can plug in a different one via the ZCA by registering a utility that implements theIRouterinterface.
To test the route, browse to:
http://localhost:8080/Plone/@@API/hello/JSON%20Plone%20API
{
"_runtime": 0.00025200843811035156,
"hello": "JSON Plone API"
}
Building URLs
When designing a RESTful JSON API you often want to include URLs to your
resources. The plone.jsonapi.core.router module provides a url_for
helper:
from plone.jsonapi.core import router
@router.add_route("/hello/<string:name>", "hello", methods=["GET"])
def hello(context, request, name="world"):
return {
"url": router.url_for(
"hello", values={"name": name}, force_external=True),
"hello": name,
}
It builds URLs using the build method of Werkzeug's MapAdapter; see
the MapAdapter.build
docs. The resulting JSON:
{
"url": "http://localhost:8080/Plone/@@API/hello/world",
"_runtime": 0.002997875213623047,
"hello": "world"
}
Permissions
The framework does not authorize requests for you (see
Architecture and scope). Every non-public
route must check its own permission. For example, to restrict the
hello route:
from AccessControl import getSecurityManager
from AccessControl import Unauthorized
from plone.jsonapi.core import router
@router.add_route("/hello/<string:name>", "hello", methods=["GET"])
def hello(context, request, name="world"):
if not getSecurityManager().checkPermission("ViewHelloAPI", context):
raise Unauthorized("You don't have the 'ViewHelloAPI' permission")
return {
"url": router.url_for(
"hello", values={"name": name}, force_external=True),
"hello": name,
}
Raising Unauthorized (or one of the typed errors from
plone.jsonapi.core.browser.exceptions, such as ForbiddenError) is
turned into a JSON error envelope with the matching HTTP status:
{
"_runtime": 0.0021250247955322266,
"success": false,
"message": "You don't have the 'ViewHelloAPI' permission",
"type": "Unauthorized"
}
Declarative permissions
Since 0.8.0 a route can declare its permission instead of checking it by
hand. Pass permission when registering the route and the router
enforces it on the dispatch context before the endpoint runs: anonymous
callers get a 401, authenticated-but-unauthorized callers a 403, both as
the usual JSON error envelope. Routes without a permission behave
exactly as before (no check), so this is fully opt-in:
from plone.jsonapi.core import router
@router.add_route("/hello/<string:name>", "hello", methods=["GET"],
permission="ViewHelloAPI")
def hello(context, request, name="world"):
return {"hello": name}
The same key works in the IRouteProvider tuple form via the options
dict, e.g. dict(methods=["GET"], permission="ViewHelloAPI"). Prefer
this over the manual check above for route-level gating; object-level
checks (against something resolved inside the endpoint) stay the
endpoint's responsibility.
Changelog
0.8.0 (2026-07-26)
- #46 Add declarative route permissions
- #44 Convert README to Markdown and document the thin-layer / permissions model
- #40 Strengthen tests and handle errors in XML responses
- #38 Drop simplejson (use stdlib json); pin werkzeug/dicttoxml to last py2.7 releases
- #35 Pre-0.8.0 cleanup: remove obsolete JSONP helper, fix logger name, trim /version, declare six dep, refresh README, bump packaging pins, drop Travis, add flake8 config
- #33 Make the router thread-safe and dispatch with a single match
- #34 Let the API receive PUT/PATCH/DELETE (WebDAV verb bypass)
- #32 Add opt-in CORS support
- #31 Convert Router match failures and no-router-matched to typed 404/405
- #30 Swappable error handler with clean envelope and status flow
- #29 Add APIError hierarchy and IErrorHandler interface
0.7.0 - 2020-03-29
- https://github.com/collective/plone.jsonapi.core/pull/23 Add Python3 compatibility
0.6 - 2017-01-10
-
Supports XML response. Use the request parameter
asxml=1or set the requestAcceptheader toapplication/xml -
https://github.com/collective/plone.jsonapi.core/issues/21 Support file streams. Use the request parameter
asbinary=1or set the requestAcceptheader toapplication/zip -
https://github.com/collective/plone.jsonapi.core/issues/22 Do not store the request on the router upon initialization
-
https://github.com/collective/plone.jsonapi.core/issues/18 Handle None values
-
https://github.com/collective/plone.jsonapi.core/issues/17 Print out Traceback's to the console and not back to the client
0.5 - 2015-07-09
-
https://github.com/collective/plone.jsonapi.core/pull/14 use
urlsplit(request.get("ACTUAL_URL", "")).netlocto get the hostname -
added more tests
-
changed info to debug logging to reduce verbosity
-
smoe minor code cleanup
0.4 - 2014-03-04
- https://github.com/ramonski/plone.jsonapi.core/issues/10 add the traceback to the response when an error occurs
- https://github.com/ramonski/plone.jsonapi.core/issues/7 started with doctests
0.3 - 2014-01-23
- renamed package to
plone.jsonapi.coredue to namespace conflicts withplone.jsonapi.routes - removed default plone route configuration.
- added
versionroute - changed the
url_formethod of the router to provide correct urls for virtual hosting.
0.2 - 2013-08-11
-
Router implementation updated to support decorated functions as route providers.
-
url_for functionality implemented
-
documentation changed
0.1 - unreleased
- initial start of development
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
File details
Details for the file plone_jsonapi_core-0.8.0.tar.gz.
File metadata
- Download URL: plone_jsonapi_core-0.8.0.tar.gz
- Upload date:
- Size: 58.5 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/1.15.0 pkginfo/1.8.3 requests/2.27.1 setuptools/42.0.2 requests-toolbelt/1.0.0 tqdm/4.64.1 CPython/2.7.18
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
6e2fba0163d2dcedef864ccaaf6081fa3f90d3facb53d4c51e45b91b67e8142d
|
|
| MD5 |
af4432c3d06d867d3565dcc64d74abc9
|
|
| BLAKE2b-256 |
6b5e2e1184447f63ed04ae5331a2f92d881ceee148534e37959e4d8fac69abd7
|