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
Geotrek, histoire d'un projet libreGeotrek, histoire d'un projet libre 31/10/2013

Retour sur les publications issues du projet

La GMAO JOB en Guadeloupe 08/10/2013

Après ALMA Services, nous voici chez EIB en Guadeloupe qui se dote de JOB, la Gestion de ...

Python async/await: introduction 10/07/2015

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

Formation Python scientifique du 14 au 18 mars à Nantes, Toulouse ou ParisFormation Python scientifique du 14 au 18 mars à Nantes, Toulouse ou Paris 29/01/2016

Cette formation vous permet de découvrir et utiliser les principales librairies de calcul ...

Formation Python initiation à Toulouse, Nantes et ParisFormation Python initiation à Toulouse, Nantes et Paris 26/01/2016

Vous êtes développeur et maîtrisez déjà un langage de programmation ? Python vous tente et ...