Accueil / Blog / Métier / 2013 / Multi-format RESTful APIs with Cornice

Multi-format RESTful APIs with Cornice

Par Alex Marandon publié 15/11/2013, édité le 16/11/2013
How to create a RESTful web API that support writing and reading multiple formats using Cornice and Pyramid

Cornice is a library from the Mozilla folks that makes it really easy to create RESTful web services in Python, using the Pyramid web framework. Cornice was able to send responses in multiple formats using content negotiation based on the Accept header. Recently Makina Corpus made a contribution that unlocked the potential of creating APIs that can also receive requests in multiple formats.

Cornice comes with a Pyramid template that allows to create a project directory using Cornice in one command:

$ pcreate -t cornice myapp

In the freshly created project directory, we'll find a file myapp/, which already contains a really simple service that returns a JSON response:

hello = Service(name='hello', path='/', description="Simplest app")

def get_info(request):
    """Returns Hello in JSON."""
    return {'Hello': 'World'}

Let's start a development server (you might need to install waitress first):

$ python develop
$ pserve --reload myapp.ini

And check that we can send a request and get a response back:

$ curl -D -
HTTP/1.1 200 OK
Content-Length: 18
Content-Type: application/json; charset=UTF-8
Date: Fri, 15 Nov 2013 10:39:41 GMT
Server: waitress

{"Hello": "World"}

Now let's say we'd like our API to send a response in plain text. We can use content negotiation with the Accept header and create a Pyramid renderer that will format a plain text response. First we create a plain text renderer. It can go in myapp/

class TextRenderer(object):

    def __init__(self, info):

    def __call__(self, value, system):
        request = system.get('request')
        if request is not None:
            response = request.response
            response.content_type = 'text/plain'
        name = value['Hello']
        return u"Hello, {}!".format(name)

We need to register this new renderer in our app main function:

def main(global_config, **settings):
    config = Configurator(settings=settings)
    config.add_renderer('text', TextRenderer)
    return config.make_wsgi_app()

Now we update our service so that it uses the correct renderer based on the Accept header:

@hello.get(accept='text/plain', renderer='text')
@hello.get(accept='application/json', renderer='json')
def get_info(request):
    """Returns Hello in JSON."""
    return {'Hello': 'World'}

Let's check that it works:

$ curl -D - -H 'Accept: text/plain'
HTTP/1.1 200 OK
Content-Length: 13
Content-Type: text/plain; charset=UTF-8
Date: Fri, 15 Nov 2013 11:22:41 GMT
Server: waitress

Hello, World!

$ curl -D - -H 'Accept: application/json'
HTTP/1.1 200 OK
Content-Length: 18
Content-Type: application/json; charset=UTF-8
Date: Fri, 15 Nov 2013 11:22:48 GMT
Server: waitress

{"Hello": "World"}

This is fine but now we'd like to produce a response based on user input. We're going to allow our users to provide their name in plain text or in JSON. We also want to validate user input to make sure the name we receive is at least two characters long. Cornice integrates well with the Colander validation library Make sure you have at least colander 1.0b1 installed.

Let's create a colander schema to implement our validation rule:

import colander

class NameSchema(colander.MappingSchema):
    name = colander.SchemaNode(colander.String(),

Then we write a POST request handler that is wired up with our Colander schema:'text/plain', renderer='text', schema=NameSchema)'application/json', renderer='json', schema=NameSchema)
def post_info(request):
    """Returns Hello in JSON."""
    name = request.validated['name']
    return {'Hello': name}

Let's see how this works if I post JSON containing my name:

$ curl -H 'Accept: application/json' -d '{"name": "Alex"}'
{"status": "error", "errors": [{"location": "body", "name": "name", "description": "name is missing"}]}

This isn't working because we haven't told the server what format we were using. If we don't specify the format, Cornice will assume we're sending application/x-www-form-urlencoded, which is the format sent by web browsers when we submit a form. So this will work:

$ curl -H 'Accept: application/json' -d 'name=Alex'
{"Hello": "Alex"}

To get Cornice to validate JSON input properly, we need to specify the Content-Type header:

$ curl -H 'Accept: application/json' -H 'Content-Type: application/json' -d '{"name": "Alex"}'
{"Hello": "Alex"}

By the way, does our validation rule work as expected?

$ curl -H 'Accept: application/json' -H 'Content-Type: application/json' -d '{"name": "A"}'
{"status": "error", "errors": [{"location": "body", "name": "name", "description": "Shorter than minimum length 2"}]}

Perfect! But remember that we'd also like to be able to send our name in plain text as the request body, without any structured format around it:

$ curl -H 'Accept: application/json' -H 'Content-Type: text/plain' -d 'Alex'
{"status": "error", "errors": [{"location": "body", "name": "name", "description": "name is missing"}]}

This is not working because out of the box Cornice can only validate JSON and form-urlencoded data. To tell our app how to interpret plain text, we need to add a deserializer. A deserializer is just a callable taking a request argument and returning a Python data structure that Colander can understand (a cstruct). In this case the callable simply returns a dictionary containing a single entry name whose value is the request body:

def plain_text_deserializer(request):
    # Here we could be parsing XML or any complex format as long as we're
    # returning a dictionary with the same structure
    return {'name': request.body}

And we assign our new deserializer to our POST handler:'text/plain', renderer='text', schema=NameSchema,
            deserializer=plain_text_deserializer)'application/json', renderer='json', schema=NameSchema)
def post_info(request):
    """Returns Hello in JSON."""
    name = request.validated['name']
    return {'Hello': name}

And now we can post plain text:

$ curl -H 'Accept: text/plain' -H 'Content-Type: text/plain' -d 'Al'
Hello, Al!

So this is basically how we implement read/write multi-format APIs in Cornice with Colander validation: we make sure we have appropriate renderers and deserializers for each format and we plug them into our view configuration. Of course in any real-world scenario your renderers and deserializers will be much more complex.

Thank you for reading so far and special thanks to Alexis Métaireau for merging our work on deserializers.

Voir aussi
Présentation de l'écosystème Python scientifique Présentation de l'écosystème Python scientifique 10/11/2016

Au fil des années Python est devenu un outil du quotidien pour les ingénieurs et chercheurs de ...

Monkey-patching a Python instance method 09/11/2016

Dynamically adding or overwriting an instance method in Python is rarely needed, but it's a good ...

Retour sur PyconFR 2013 05/11/2013

L'édition 2013 de la conférence Python française se tenait à Strasbourg du 26 au 29 Octobre. Je ...

Retour sur PyConFr 2015 Retour sur PyConFr 2015 19/10/2015

Makina Corpus était présent à Pau pour la PyConFr 2015, voici quelques retours à chaud.

Retour sur la PyConFr 2016 Retour sur la PyConFr 2016 18/10/2016

Nous étions présents à Rennes pour PyConFr 2016. Voici notre compte-rendu à chaud.