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.

Goals of cubicweb-jsonschema

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.

Basic usage as an HTTP API library

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
        }
    ]
}

From Yams model to JSON Schema definitions

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 [1].

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.

Conclusion

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.

[1]this choice is essentially driven by simplicity and conformance when the existing behavior to help migration of existing applications.
blog entry of