This is the first post of a series introducing the cubicweb-jsonschema
project that is currently under development at Logilab. In this post, I'll
first introduce the general goals of the project and then present in more
details two aspects about data models (the connection between Yams and JSON
schema in particular) and the basic features of the API. This post does not
always present how things work in the current implementation but rather how
they should.
From a high level point of view, cubicweb-jsonschema addresses mainly two
interconnected aspects. One related to modelling for client-side development
of user interfaces to CubicWeb applications while the other one concerns the
HTTP API.
As far as modelling is concerned, cubicweb-jsonschema essentially aims at
providing a transformation mechanism between a Yams schema and JSON Schema
that is both automatic and extensible. This means that we can ultimately
expect that Yams definitions alone would sufficient to have generated JSON
schema definitions that would consistent enough to build an UI, pretty much as
it is currently with the automatic web UI in CubicWeb. A corollary of this
goal is that we want JSON schema definitions to match their context of usage,
meaning that a JSON schema definition would not be the same in the context of
viewing, editing or relationships manipulations.
In terms of API, cubicweb-jsonschema essentially aims at providing an HTTP
API to manipulate entities based on their JSON Schema definitions.
Finally, the ultimate goal is to expose an hypermedia API for a CubicWeb
application in order to be able to ultimately build an intelligent client. For
this we'll build upon the JSON Hyper-Schema specification. This aspect will
be discussed in a later post.
Consider a simple case where one wants to manipulate entities of type Author
described by the following Yams schema definition:
class Author(EntityType):
name = String(required=True)
With cubicweb-jsonschema one can get JSON Schema for this entity type in at
different contexts such: view, creation or edition. For instance:
in a view context, the JSON Schema will be:
{
"$ref": "#/definitions/Author",
"definitions": {
"Author": {
"additionalProperties": false,
"properties": {
"name": {
"title": "name",
"type": "string"
}
},
"title": "Author",
"type": "object"
}
}
}
whereas in creation context, it'll be:
{
"$ref": "#/definitions/Author",
"definitions": {
"Author": {
"additionalProperties": false,
"properties": {
"name": {
"title": "name",
"type": "string"
}
},
"required": [
"name"
],
"title": "Author",
"type": "object"
}
}
}
(notice, the required keyword listing name property).
Such JSON Schema definitions are automatically generated from Yams
definitions. In addition, cubicweb-jsonschema exposes some endpoints for basic
CRUD operations on resources through an HTTP (JSON) API. From the client point
of view, requests on these endpoints are of course expected to match JSON
Schema definitions.
Some examples:
Get an author resource:
GET /author/855
Accept:application/json
HTTP/1.1 200 OK
Content-Type: application/json
{"name": "Ernest Hemingway"}
Update an author:
PATCH /author/855
Accept:application/json
Content-Type: application/json
{"name": "Ernest Miller Hemingway"}
HTTP/1.1 200 OK
Location: /author/855/
Content-Type: application/json
{"name": "Ernest Miller Hemingway"}
Create an author:
POST /author
Accept:application/json
Content-Type: application/json
{"name": "Victor Hugo"}
HTTP/1.1 201 Created
Content-Type: application/json
Location: /Author/858
{"name": "Victor Hugo"}
Delete an author:
DELETE /author/858
HTTP/1.1 204 No Content
Now if the client sends invalid input with respect to the schema, they'll get
an error:
(We provide a wrong born property in request body.)
PATCH /author/855
Accept:application/json
Content-Type: application/json
{"born": "1899-07-21"}
HTTP/1.1 400 Bad Request
Content-Type: application/json
{
"errors": [
{
"details": "Additional properties are not allowed ('born' was unexpected)",
"status": 422
}
]
}
The example above illustrates automatic generation of JSON Schema documents
based on Yams schema definitions. These documents are expected to help
developping views and forms for a web client. Clearly, we expect that
cubicweb-jsonschema serves JSON Schema documents for viewing and editing
entities as cubicweb.web serves HTML documents for the same purposes. The
underlying logic for JSON Schema generation is currently heavily inspired by
the logic of primary view and automatic entity form as they exists in
cubicweb.web.views. That is: the Yams schema is introspected to determine
how properties should be generated and any additionnal control over this can
be performed through uicfg declarations .
To illustrate let's consider the following schema definitions which:
class Book(EntityType):
title = String(required=True)
publication_date = Datetime(required=True)
class Illustration(EntityType):
data = Bytes(required=True)
class illustrates(RelationDefinition):
subject = 'Illustration'
object = 'Book'
cardinality = '1*'
composite = 'object'
inlined = True
class Author(EntityType):
name = String(required=True)
class author(RelationDefinition):
subject = 'Book'
object = 'Author'
cardinality = '1*'
class Topic(EntityType):
name = String(required=True)
class topics(RelationDefinition):
subject = 'Book'
object = 'Topic'
cardinality = '**'
and consider, as before, JSON Schema documents in different contexts for the
the Book entity type:
in view context:
{
"$ref": "#/definitions/Book",
"definitions": {
"Book": {
"additionalProperties": false,
"properties": {
"author": {
"items": {
"type": "string"
},
"title": "author",
"type": "array"
},
"publication_date": {
"format": "date-time",
"title": "publication_date",
"type": "string"
},
"title": {
"title": "title",
"type": "string"
},
"topics": {
"items": {
"type": "string"
},
"title": "topics",
"type": "array"
}
},
"title": "Book",
"type": "object"
}
}
}
We have a single Book definition in this document, in which we find
attributes defined in the Yams schema (title and publication_date). We
also find the two relations where Book is involved: topics and
author, both appearing as a single array of "string" items. The author
relationship appears like that because it is mandatory but not composite.
On the other hand, the topics relationship has the following uicfg rule:
uicfg.primaryview_section.tag_subject_of(('Book', 'topics', '*'), 'attributes')
so that it's definition appears embedded in the document of Book
definition.
A typical JSON representation of a Book entity would be:
{
"author": [
"Ernest Miller Hemingway"
],
"title": "The Old Man and the Sea",
"topics": [
"sword fish",
"cuba"
]
}
in creation context:
{
"$ref": "#/definitions/Book",
"definitions": {
"Book": {
"additionalProperties": false,
"properties": {
"author": {
"items": {
"oneOf": [
{
"enum": [
"855"
],
"title": "Ernest Miller Hemingway"
},
{
"enum": [
"857"
],
"title": "Victor Hugo"
}
],
"type": "string"
},
"maxItems": 1,
"minItems": 1,
"title": "author",
"type": "array"
},
"publication_date": {
"format": "date-time",
"title": "publication_date",
"type": "string"
},
"title": {
"title": "title",
"type": "string"
}
},
"required": [
"title",
"publication_date"
],
"title": "Book",
"type": "object"
}
}
}
notice the differences, we now only have attributes and required relationships
(author) in this schema and we have the required listing mandatory
attributes; the author property is represented as an array which items
consist of pre-existing objects of the author relationship (namely
Author entities).
Now assume we add the following uicfg declaration:
uicfg.autoform_section.tag_object_of(('*', 'illustrates', 'Book'), 'main', 'inlined')
the JSON Schema for creation context will be:
{
"$ref": "#/definitions/Book",
"definitions": {
"Book": {
"additionalProperties": false,
"properties": {
"author": {
"items": {
"oneOf": [
{
"enum": [
"855"
],
"title": "Ernest Miller Hemingway"
},
{
"enum": [
"857"
],
"title": "Victor Hugo"
}
],
"type": "string"
},
"maxItems": 1,
"minItems": 1,
"title": "author",
"type": "array"
},
"illustrates": {
"items": {
"$ref": "#/definitions/Illustration"
},
"title": "illustrates_object",
"type": "array"
},
"publication_date": {
"format": "date-time",
"title": "publication_date",
"type": "string"
},
"title": {
"title": "title",
"type": "string"
}
},
"required": [
"title",
"publication_date"
],
"title": "Book",
"type": "object"
},
"Illustration": {
"additionalProperties": false,
"properties": {
"data": {
"format": "data-url",
"title": "data",
"type": "string"
}
},
"required": [
"data"
],
"title": "Illustration",
"type": "object"
}
}
}
We now have an additional illustrates property modelled as an array of
#/definitions/Illustration, the later also added the the document as
an additional definition entry.
This post illustrated how a basic (CRUD) HTTP API based on JSON Schema could
be build for a CubicWeb application using cubicweb-jsonschema. We have seen
a couple of details on JSON Schema generation and how it can be controlled.
Feel free to comment and provide feedback on this feature set as well as
open the discussion with more use cases.
Next time, we'll discuss how hypermedia controls can be added the HTTP API
that cubicweb-jsonschema provides.