I recently needed to implement a special kind of field in Django Rest Framework that turned out to be a fun
experiment. I recreated this in a simple Django application, and the bulk of the DRF-related code can be found
here.
First, the API used a hashed ID. So the primary key from the database was taken, additional information added
to it and then hashed.
Second, the API to set the relationship value behaved differently than when reading it. When sending a POST
or PUT value, the API client would always send an object with the id key and the hashed ID of the
relationship. This is different than most of the existing relationship fields in DRF, where the field is
a scalar of some type. Along with this custom write pattern, on read operations the nested object was expected
to be rendered with all fields.
An example is helpful to see the pattern. In this example we have two models, Author and Book, where a book
has one Author relationship.
Creating a new book would look something like
1
2
3
4
5
6
{"title":"Awesome new book","author":{"id":"hashed1",}}
And the response from this would be something like the below. You can see that by having the API send the ID as part
of an object, instead of as a scalar, the request and response begin to look similar, which is pretty cool.
1
2
3
4
5
6
7
{"title":"Awesome new book","author":{"id":"hashed1","name":"Arthur Writes"}}
Most of the work was around creating new field types for these hashed IDs as well as one specifically for wrapping it
in an object.
The first part was pretty easy, getting the IDs into a hashed format. This was two classes, one used for the ID on the
current object and the other used for the relationship fields.
classHashedPrimaryKeyField(serializers.CharField):"""
A field representing the primary key of an object as a base64 encoded string.
"""defto_internal_value(self,data):returnbase64.urlsafe_b64decode(data).decode('utf-8')defto_representation(self,value):returnbase64.urlsafe_b64encode(bytes(f'{value}','utf-8')).decode('utf-8')classHashedPrimaryKeyRelatedField(serializers.PrimaryKeyRelatedField):"""
A relationship field representing the primary key of a related field as a base64 encoded string.
"""defto_internal_value(self,data):data=base64.urlsafe_b64decode(data).decode('utf-8')returnsuper().to_internal_value(data)defto_representation(self,value):ifvalueisNone:returnvaluereturnbase64.urlsafe_b64encode(bytes(f'{value.pk}','utf-8')).decode('utf-8')
The next challenge was to determine the format which the user requested. I left this up to the DRF content negotiation.
The negotiation sets an accepted_renderer parameter on the request before passing it down to the serializer in the
APIView. This means determining the format is as easy as
Next, I updated the validation and representational logic for my new field. Because my representation uses only the
ID when building a non-JSON request, I turned on the PK only optimization in those cases and turned it off otherwise.
defrun_validation(self,data=None):ifself._is_json()anddataisnotNone:ifnotisinstance(data,dict):self.fail('id_missing')if'id'notindata:self.fail('id_missing')returnsuper().run_validation(data=data)defuse_pk_only_optimization(self):ifself._accepts_json:returnFalsereturnTruedefto_internal_value(self,data):ifself._is_json()anddataisnotNone:data=data['id']returnsuper().to_internal_value(data)defto_representation(self,value):ifself._accepts_json():# I pass the serializer class to the field on construction to make this possiblereturnself.serializer(instance=value).datareturnsuper().to_representation(value)
Finally, I updated my serializers to use the new fields.