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

Multi-format RESTful APIs with Cornice

Par Alex Marandon 15/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.

Mots-clés associés : ,
Voir aussi
Optimiser ses tests unitaires Django avec setUpTestDataOptimiser ses tests unitaires Django avec setUpTestData 22/06/2015

Découvrez comment gagner en efficacité sur les tests unitaires avec Django 1.8 et sa nouvelle ...

Introduction to Python async/await 10/07/2015

Python 3.5 is coming up soon with async and await built-in keywords. Let's get excited with a ...

Alerte à la pollution en Méditerranée ! 22/10/2008

Makina Corpus réalise pour le Centre Régional Méditerranéen pour l’Intervention d’Urgence ...

AFPyro National du 28 juillet 2010 22/07/2010

Les AFPyros sont ces rencontres informelles autour d'un verre dont le prétexte est de discuter de ...

Le World Plone Day à Bruxelles, Nantes, Pau et Toulouse 10/11/2008

Le World Plone Day est un événement mondial qui se déroule dans plus de 59 villes sur 31 pays de ...