Lets say I have three related models - Container, Category and Item. Each model has two string properties - a name and an uuid (as well as the normal "id" field.) The uuid fields will be used as SlugRelatedFields to express relationships in the REST interface. A Container contains a set of items and a set of categories; each item is related to a category.
I'd like to create an entire container (with all its items and categories) in one POST. But I can't seem to create a serializer that allows that. When I try to do so, I get an error when the serializer tries to decode an item - it finds that the referenced category doesn't exist yet. Which is not surprising, since the same POST is going to create it.
In JSON form, here's what I'd like to POST:
{
"name": "My Container",
"uuid": "<container-uuid>",
"categories": [
{
"name": "Category One",
"uuid": "<category-one-uuid>",
}
],
"items": [
{
"name": "Item One",
"uuid": "<item-one-uuid>",
"category": "<category-one-uuid>",
},
{
"name": "Item Two",
"uuid": "<item-two-uuid>",
"category": "<category-one-uuid>",
}
]
}
So that example would create one container, one category and two items - both items would be related to the same category.
I can use a SlugRelatedField in the Item serializer to identify the associated Category by uuid. But in this scenario, that throws an error:
Object with uuid=<category-one-uuid> does not exist.
while deserializing "Item One". Understandably, since that category is not yet in the database.
Is there some way to make this work? Can I override some method in the ItemSerializer to make it look for the associated Category in the Container that is being deserialized, rather than looking at the Categories that already exist in the database? I'm thinking that a solution may require a couple of things:
- Ensuring that the categories are de-serialized before the items. What determines that? The order of the "fields" attribute in the serializer?
- Using the deserialized categories a create a queryset that is used during deserialization of the items, replacing the database query that is normally used.
Any ideas?
Update 2014-08-22:
I haven't fixed this problem, but I've worked around it. The workaround is a filthy hack, involving a number of parts:
In the model, I changed the definition of the foreignkey reference from item to category to allow NULL.
In the serializer for Item, I declared the category as a simple CharField(), rather than a SlugRelatedField that identifies the Category by its UUID.
In that serializer's "restore_object" method, I grab the specified UUID out of the category field (in the "attrs" dictionary passed to that method). I remove the entry from that dictionary and add an entry to a dictionary that maps Item UUIDs to the associated Category UUIDs. That dictionary is held in a per-request cache, as described here: Per-request cache in Django?
Then I have a handler for the "pre_save" signal on item, and it looks up the required category UUID in that dictionary (using the Items's UUID as key), does a query on the Category table to find the right item and uses that to set the foreignkey field.
So, when a request comes in to create a new Container with associated Items and Categories:
- Because the Category UUID reference in the Item is just a CharField, it is NOT verified against a query of the existing categories in the database. So the serializer proceeds to saving stuff in the database.
- The Categories are saved to the DB as normal. (They get saved first, presumably just because of the order of the Seriliazer's "fields" array.)
- When the "restore_object" method is used to construct the Item that will be saved, the UUID of the required Category is stashed away for later use, and that "CharField" value is discarded from the fields used to construct the Item.
- When the Item is about to be saved, the signal handler retrieves the stashed UUID and uses it to fetch the correct (newly-created) category to fill out the foreignkey field.
Hacky as heck, right? But it seems to work. I'd love to rework this one day to use a more elegant solution but in the real world, that won't happen.