One day at work I had to implement JSON response on simple GET request. I was just going to code another RESTish bicycle as my project manager told me to use
When spec for my task was being written, author didn't think about what tool will be used for implementation. On the other side tastypie authors didn't know what people will do with their lib. So my task was to make this two worlds meet in one place. On github project page there's a simple example with very basic things showed. The real world is usually more complicated. I'll try to show you how I worked with tastypie.
We have some working web application and we need to add REST API to it. Our main models are: Cabinet, Folder and File. Cabinets stand in some office. Every Cabinet has Folders inside, which contains Files of different types.
The task is to implement such response for request
This means that we need whole tree of entries, gathered in one root called 'Cabinets'.
Installing is easy and usual -
After reading docs we create first resource that looks like:
Docs say that we should put this in api.py file in app folder.
Then we should create url handler:
This creates a resource at
Good start but we need more!
We got
But first let's deal with URI of our resource. Our task is clear - URI must be
While adding subelements to response I tried some weird hacks. But it turned out to be quite simple.
Connections are established between resources, not models. So here's a resource for Cabinet's child class - Folder:
Now we need to add field to CabinetResource, what result in something like:
Optional parameter
Having always empty parameter isn't cool. So I wanted to turn them off. First I tried hackish way, deleting this parameter manually on response generation step. But then I noticed a parameter
And at last we would like to remove field
So now our
And response at
Next step is to add subelements of Folders - Files. We already know how to do it, so let's make it quick.
Adding a resource:
And a field to Folder resource:
And now we have a response like this (excerpt):
Almost there. One issue left about Files model. It has field FileType that tells us quite useful information. It is implemented as ForeighKey, but our specification tells us to integrate it flat, without creating another sublevel. What we can do with it? Let's first create regular subresource.
That responses with
We have
but we want only
Hopefully
To be clear,
django-tastypie
. I'm sure he knows what he is talking about so I opened google to find what tastypie is.When spec for my task was being written, author didn't think about what tool will be used for implementation. On the other side tastypie authors didn't know what people will do with their lib. So my task was to make this two worlds meet in one place. On github project page there's a simple example with very basic things showed. The real world is usually more complicated. I'll try to show you how I worked with tastypie.
Useful link
A Brief Introduction to REST
Let's take an example. For this post I created tine django project which you can find at github. A Brief Introduction to REST
We have some working web application and we need to add REST API to it. Our main models are: Cabinet, Folder and File. Cabinets stand in some office. Every Cabinet has Folders inside, which contains Files of different types.
The task is to implement such response for request
GET api/cabinet/list
{
"cabinets":[
{
"added":"2011-06-17",
"color":"black",
"floor":2,
"folders":[
{
"files":[
{
"archive_num":"221122",
"content":"Lorem ipsum dolor sit amet
consectetur adipisicing elit",
"type":"Evidence"
}
],
"name":"XFiles",
"secrecy":10
},
{
"files":[
{
"archive_num":"1111",
"content":"sunt in culpa qui officia deserunt mollit
anim id est laborum",
"type":"Document"
},
{
"archive_num":"333",
"content":"$1000",
"type":"Bill"
}
],
"name":"Foldy folder",
"secrecy":6
}
],
"height":2,
"width":10
},
{
"added":"2011-06-17",
"color":"white",
"floor":10,
"folders":[
{
"files":[
{
"archive_num":"87878",
"content":"Funny picture",
"type":"Picture"
}
],
"name":"Fun pics",
"secrecy":0
},
{
"files":[
{
"archive_num":"23423424",
"content":"Photo of some big room",
"type":"Photo"
}
],
"name":"Old folder",
"secrecy":3
}
],
"height":1,
"width":4
}
]
}
This means that we need whole tree of entries, gathered in one root called 'Cabinets'.
Installing is easy and usual -
pip install django-tastypie
.After reading docs we create first resource that looks like:
from tastypie.resources import ModelResource
from app.models import Cabinet
class CabinetResource(ModelResource):
class Meta:
queryset = Cabinet.objects.all()
Docs say that we should put this in api.py file in app folder.
Then we should create url handler:
from app.api import CabinetResource
cabinet_resource = CabinetResource()
urlpatterns = patterns('',
...
url(r'^api/', include(cabinet_resource.urls))
...
)
This creates a resource at
/api/cabinet
which returns:{
"meta":{
"limit":20,
"next":null,
"offset":0,
"previous":null,
"total_count":2
},
"objects":[
{
"added":"2011-06-17",
"color":"black",
"floor":2,
"height":2,
"id":"1",
"resource_uri":"/api/cabinet/1/",
"width":10
},
{
"added":"2011-06-17",
"color":"white",
"floor":10,
"height":1,
"id":"2",
"resource_uri":"/api/cabinet/2/",
"width":4
}
]
}
Good start but we need more!
We got
Meta
object, that we don't want to get :), we have resource_uri
and id
, which we didn't ask either, and most important - we want elements, connected to our Cabinets.But first let's deal with URI of our resource. Our task is clear - URI must be
api/cabinet/list
. Resource Meta class has field resource_name
, which is clearly described in docs. By default resource name is generated from resource class name by removing word Resource and lowercasing it. But you can specify your own resource name and nothing keeps you from making it as custom as you want, for example - list/cabinet
(api
part is defined at urls.py), right what we need!While adding subelements to response I tried some weird hacks. But it turned out to be quite simple.
tastypie
doesn't make connections automatically. Authors follow the principle "Explicit is better then implicit". So every connection of your resource must be done manually.Connections are established between resources, not models. So here's a resource for Cabinet's child class - Folder:
class FolderResource(ModelResource):
class Meta:
queryset = Folder.objects.all()
Now we need to add field to CabinetResource, what result in something like:
class CabinetResource(ModelResource):
folders = fields.ToManyField('app.api.FolderResource',
'folders', full=True)
class Meta:
.....
Optional parameter
full
is False by default. In this state folders
subelement will only contain links to it's respective Folder resources. To work this way you need to extend your urlhandlers with FolderResource urls. This is described quite well in official docs. But in our case we need to show not just a link, but whole object's content. To achieve it we set full=True
. Here's little trick I've noticed while was writing this post. If you don't specify urlhandlers for subelements (Folder in our case) resource_uri parameter for subelements will be empty. So nobody would have a way to get your objects (via this API, I mean)Having always empty parameter isn't cool. So I wanted to turn them off. First I tried hackish way, deleting this parameter manually on response generation step. But then I noticed a parameter
include_resource_uri
which prevents adding resource_uri
parameter to object.And at last we would like to remove field
id
. Showing to the world inner data like ID of object in database is potentially dangerous. This could be done with fields
or excludes
parameters, that might be familiar to you from Django Forms, that use the same principle. To hide id
field we add excludes = ['id']
to our Meta classes.So now our
api.py
file is:from tastypie.resources import ModelResource
from tastypie import fields
from app.models import Cabinet, Folder
class FolderResource(ModelResource):
class Meta:
queryset = Folder.objects.all()
excludes = ['id']
include_resource_uri = False
class CabinetResource(ModelResource):
folders = fields.ToManyField('app.api.FolderResource',
'folders', full=True)
class Meta:
queryset = Cabinet.objects.all()
resource_name = 'cabinet/list'
excludes = ['id']
include_resource_uri = False
And response at
/api/cabinet/list
is{
"meta":{
"limit":20,
"next":null,
"offset":0,
"previous":null,
"total_count":2
},
"objects":[
{
"added":"2011-06-17",
"color":"black",
"floor":2,
"folders":[
{
"name":"XFiles",
"secrecy":10
},
{
"name":"Foldy folder",
"secrecy":6
}
],
"height":2,
"width":10
},
{
"added":"2011-06-17",
"color":"white",
"floor":10,
"folders":[
{
"name":"Fun pics",
"secrecy":0
},
{
"name":"Old folder",
"secrecy":3
}
],
"height":1,
"width":4
}
]
}
Next step is to add subelements of Folders - Files. We already know how to do it, so let's make it quick.
Adding a resource:
class FileResource(ModelResource):
class Meta:
queryset = File.objects.all()
excludes = ['id']
include_resource_uri = False
And a field to Folder resource:
class FolderResource(ModelResource):
files = fields.ToManyField('app.api.FileResource',
'files', full=True)
class Meta:
...
And now we have a response like this (excerpt):
....
"objects":[
{
"added":"2011-06-17",
"color":"black",
"floor":2,
"folder":[
{
"file":[
{
"archive_num":"221122",
"content":"Lorem ipsum dolor sit amet, consectetur"
}
],
"name":"XFiles",
"secrecy":10
},
{
"file":[
{
"archive_num":"1111",
"content":"Duis aute irure dolor in reprehenderit
in voluptate velit esse"
},
{
"archive_num":"333",
"content":"$1000"
}
],
"name":"Foldy folder",
"secrecy":6
}
],
"height":2,
"width":10
},
....
Almost there. One issue left about Files model. It has field FileType that tells us quite useful information. It is implemented as ForeighKey, but our specification tells us to integrate it flat, without creating another sublevel. What we can do with it? Let's first create regular subresource.
class FileTypeResource(ModelResource):
class Meta:
queryset = FileType.objects.all()
class FileResource(ModelResource):
type = fields.ToOneField('app.api.FileTypeResource',
'type', full=True)
class Meta:
...
That responses with
...
"files":[
{
"archive_num":"221122",
"content":"Lorem ipsum dolor sit amet, consectetur adipisicing elit",
"type":{
"id":"4",
"resource_uri":"",
"type":"Evidence"
}
}
],
...
We have
"type":{
"id":"4",
"resource_uri":"",
"type":"Evidence"
}
but we want only
"type":"Evidence"
.Hopefully
tastypie
has quite a lot of handy hooks that you can use to modify default behaviour. What we need now is dehydrate
method. It is called every time when object is prepared for serialization. dehydrate
receives two parameters:self
- obviously, current resource object linkbundle
- object of Bundle class. It has two main fields:
obj
- a link to object that is being serializeddata
- dictionary with content ofobj
prepared for serialization
dehydrate
simply returns bundle
, which is afterwards serialized into JSON dictionary (or other structure with subelements if response is not JSON). So, basically, everything we need is dehydrate
to return one bit of data - FileType.type class FileTypeResource(ModelResource):
class Meta:
queryset = FileType.objects.all()
def dehydrate(self, bundle):
return bundle.data['type']
dehydrate
is the right place to manipulate with data you're going to return to your client. Let's deal with what is left! Default response consist object meta
. It is generally useful to have some meta data attached to response. Without it things can become confusing when using offsets or other request modifiers. Nonetheless, sometimes we just have to get rid of it. There's a method for that! Method is called alter_list_data_to_serialize
and it starts after all dehydrations, right before serialization. We have to add it to our root CabinetResource
. : def alter_list_data_to_serialize(self, request, data_dict):
if isinstance(data_dict, dict):
if 'meta' in data_dict:
# Get rid of the "meta".
del(data_dict['meta'])
return data_dict
To be clear,
dehydrate
works on per-resource level, when alter_list_data_to_serialize
works with results of dehydrate
— generated python lists and dicts. And the last thing. Now root element is called 'objects' and I don't like it. To rename we add following to alter_list_data_to_serialize
...
def alter_list_data_to_serialize(self, request, data_dict):
if isinstance(data_dict, dict):
if 'meta' in data_dict:
# Get rid of the "meta".
del(data_dict['meta'])
# Rename the objects.
data_dict['cabinets'] = copy.copy(data_dict['objects'])
del(data_dict['objects'])
return data_dict
Useful link
Mobile API Design - Thinking Beyond REST
not just for mobile, but for overall good REST API design
Renaming is simple, though I still think there must be some option to set it without doing not-so-good things like this. That's it. There's a ton of features I didn't cover. This post is already much larger then I expected. I have to stop here or I will never publish it :)Mobile API Design - Thinking Beyond REST
not just for mobile, but for overall good REST API design
No comments:
Post a Comment
Thanks for your comment!
Come back and check response later.