Repository: django-json-api/django-rest-framework-json-api

Issue:
Unable to render regular Serializer (missing PK)
It appears that the JSON renderer expects every serializer to be a `ModelSerializer`, preventing regular `Serializer` use without a model.

**Reproduction**:

```py
from typing import TYPE_CHECKING

from rest_framework.permissions import AllowAny
from rest_framework.fields import CharField, Field
from rest_framework.views import Response
from rest_framework.viewsets import GenericViewSet
from rest_framework_json_api.serializers import Serializer

from .serializers import HealthStatusSerializer

if TYPE_CHECKING:
    from rest_framework. request import Request


class HealthStatusSerializer(Serializer):
    """The health status of all services"""

    def get_fields(self) -> dict[str, Field]:
        return {"Database: default": CharField()}  # for demo purposes - usually populate from installed health checks

    class Meta:
        resource_name = "HealthStatus"


class HealthStatusAPIView(GenericViewSet):
    """A health status view"""

    serializer = HealthStatusSerializer
    permission_clases = [AllowAny]
    pagination_class = None  # remove pagination
    filter_backends = []  # remove default filters

    def retrieve(request: Request, **kwargs) -> Response:
        """Retrieves the health status of all services"""

        errors: bool = False  # for demo purposes - usually check status of installed health checks
        return Response(
            self.get_serializer({"Database: default": "working"}).data,
            status_code=200 if not errors else 500,
        )
```

**Stacktrace**:

```
Traceback (most recent call last):
  File ".../django/core/handlers/exception.py", line 55, in inner
    response = get_response(request)
  File ".../django/core/handlers/base.py", line 220, in _get_response
    response = response.render()
  File ".../django/template/response.py", line 114, in render
    self. content = self.rendered_content
  File ".../rest_framework/response.py", line 70, in rendered_content
    ret = renderer. data, accepted_media_type, context)
  File ".../rest_framework_json_api/renderers.py", line 602, in render
    json_api_data = self.build_json_resource_obj(
  File ".../rest_framework_json_api/renderers.py", line 464, in build_json_resource_obj
    encoding. force_str(resource_instance.pk) if resource_instance else None,
AttributeError: 'dict' object has no attribute 'pk'
```

**Suspected code**:

https://github.com/django-json-api/django-rest-framework-json-api/blob/735ce291d7385fea39b5a50c107449c2d8fdd3d9/rest_framework_json_api/renderers.py#L460-L467

**Suggestion**:

``` diff
        resource_data = [
            ("type", resource_name),
            (
                "id",
-                encoding.force_str(resource_instance.pk) if resource_instance else None,
+                encoding.force_str(resource_instance.pk) if hasattr(resource_instance, "pk") else None,
            ),
            ("attributes", cls.extract_attributes(fields, resource)),
        ]
```
Support custom model object keys
Hey, thanks for your work on this project. I was investigating integrating it with our EmberJS app but I ran into an issue. We have an `Employee` model that has a normal integer primary key, but also a `username` attribute. I'd like to only expose the `username` as the primary key, using DRF this is simple:

``` python
class EmployeeSerializer(serializers.ModelSerializer):
    id = serializers.ReadOnlyField(source="user.username")
    ...
```

However in `build_json_resource_obj` it just uses instance.pk:

``` python
def build_json_resource_obj(fields, resource, resource_instance, resource_name):
    resource_data = [
        ('type', resource_name),
        ('id', encoding.force_text(resource_instance.pk) if resource_instance else None),
        ...
```

Can you use the serializers ID field if available, and fallback to the instances PK?



Discussion:

So your `Employee` model has an integer primary key but you're overwriting it on output to use another field's (`username`) value? That sounds like a rocky road to go down as I'm pretty sure relationships will fail since you're masquerading a non primary key value as a primary key.

Would it be possible for you to change your model's `username` field to `primary_key=True` and get rid of `id` or better yet change your `id` field to a `uuid`? (the latter is what I do, Django 1.8 introduced core support for UUID pks).

I'm a little hesitant to change `build_json_resource_obj` right off because, at least in my mind, the `id` should always be a primary key. I'm open to a discussion on the matter though

I agree it sounds rocky, but in the context of the app it makes sense. We have an app with a django user model that is backed by LDAP, so the username field is the 'true' primary key and the integer id is an implementation detail for that application. The Emberjs app pulls in data from this system and two others, so exposing the primary key seems risky as its just the ID for this particular row, the username is much safer to share between the systems.

Also we view exposing the user id as a security risk, it opens the system up to easy enumeration attacks whereas the username is not susceptible without a large brute forcing effort. Upgrading the central app would be a large undertaking, and we've been using this method for a while without any issues with the DRF ember package. Upgrading the app to use the username as the primary key would be awesome, but it's a fairly critical application and that would be a large/costly undertaking with already stretched resources.

Surely it's not too much to just use the `id` field specified in the serializer, it seems a weird limitation to have. We're also all consenting adults here, if someone is exposing a custom ID then they hopefully know what they are doing :+1: (and if not then it's their fault, not you or this packages).

How would you deal with the relations then? What is the ID used in other objects ForeignKeys?

There are two or three models being served from the same application, for those foreign keys we just use a CharField with `source='user_relation.username'`. For the relations stored on other systems we just store the username in a charfield, then the Ember app issues a lookup for `/api/employees/user.id/`.

Honestly, its also a bit about enumeration. With ~500 employees (past and present) in the database exposing the integer userID makes it super super easy to gather a big list of everyone who has ever worked for us. From this perspective exposing the username is a much much better approach.

@orf This behavior is completely out of the scope of this library. 
Also, security by obscurity is not a good way to protect your data. Also like @jerel mentioned switching to uuids could meet both ends and solve this for you.

Upgrading a huge, critical LOB application to use GUIDs (which is only more secure by virtue of being a larger keyspace) is a huge undertaking, whereas this is a genuine use case and what I think is a somewhat simple fix. There are many cases where you may not want to expose the internal ID of an object but instead use another `unique` field, and I really think it would be a good idea to support this rather than just assuming everyone wants the `pk` exposed and offering absolutely no way to change it (other than monkeypatching, which is icky).

But it's your library and your choice. I'm more than willing to make a MR with the changes and have another discussion based on that?

@orf we are always open to PRs and I did not want to offend you in any ways. it's just that with my knowledge of the internals of DRF, using another field as the ID for a relation, just seems to be a lot of work and one would need to rewrite a lot of things for a very specific edge-case.
My 0.02$.

Again feel free to open a PR if you come up with a solution.

I think I'm misunderstanding something about your use case. Isn't supporting alternative primary keys what the `pk` object on django model instances is for in the first place? So that we don't have to hardcode references to `instance.id`? For example, the following model uses a CharField as a primary key:

```
class MyModel(models.Model):
    foo = CharField(primary_key=True)
    # ... other fields here
```

Now `instance.pk` is an alias for `instance.foo` on instances of `MyModel`.

If your username field is unique anyway, what's the barrier to simply setting `primary_key=True` on the username field?

Then third-party code like `rest_framework_json_api` and any other third-party django apps you might use would have no problem retrieving your user instances by the username field instead of the id field, without having to modify the code at all.

Oh, I realize that the 'username' field is on a different model. Assuming the username field is unique on the user model, and if no two Employees can have a foreign key to the same user (i.e. the foreign key to user on Employee is unique), you could have the Employee.user foreign key reference user.username (instead of user.id) and then set that foreign key field as Employee's primary key.

So Employee.user would look like:

``` python
user = models.ForeignKey(to_field='username', primary_key=True)
```

And if `employee` is in an instance of `Employee`, `employee.pk` would return the username.

This would involve one schema migration (to adjust the foreign key constraints) and no data migrations.

Hey, yeah in our case the app is still using the old Django profile system rather than a custom user model, but that's not entirely relevant to the issue I'm reporting. I don't think I explained this properly  above, sorry!

DRF allows you to expose any attribute you feel fit as the 'id' in a serializer. By default it uses the 'pk' attribute of the model, but this can be changed (see below). All I would like is for this functionality to be exposed by this library, currently it's hard-coded to the `pk` of the model. In merge request #165 I've taken a stab at it by simply grabbing the `pk` field from the serializers output, which it currently ignores and just uses `model.pk`.

``` python
class ThingSerializer(ModelSerializer):
    id = serializers.CharField(source='attribute')

    class Meta:
        fields = ('id',)
        model = Thing

class RelatedThing(ModelSerializer):
    # produces a list of the charfield attributes defined above, rather than integer PK's
    some_relation = ThingSerializer(many=True)  
    ...
```

My particular use case is a bit convoluted, but the core of it is that this library naively assumes that you only want to expose the `pk` of a model as is, when DRF allows you to expose anything and as long as your serializers are wired up correctly then all the CRUD actions work fine.

If anyone's interested, I've added support for defering to `lookup_field`/custom serializer `id` fields such that the DB can still use the defaul/some other pk field/column, while the API can use another field, such as UUID, throughout.
Thank you! 
Hey @rpatterson are you able to make a MR with that change?
Hi everyone,

What is the status of this? Is it going to be merged eventually? It would be great to be able to use custom fields for API lookups and model PKs for model relationships.
This merge request is unlikely to be merged as I have received no feedback
from the maintainers regarding this, and there are conflicts.

To reiterate, there is absolutely no technical reason why this cannot be
supported. The underlying issue is that this library, unlike django rest
framework, hard codes the primary key field in a few places and incorrectly
assumes you always want to use that. Rest framework does not hard code this
and correctly uses the model serialiser to identify the primary key field.

On 12 May 2017 17:55, "Luca Corti" <notifications@github.com> wrote:

> Hi everyone,
>
> What is the status of this? Is it going to be merged eventually? It would
> be great to be able to use custom fields for API lookups and model PKs for
> model relationships.
>
> —
> You are receiving this because you were mentioned.
> Reply to this email directly, view it on GitHub
> <https://github.com/django-json-api/django-rest-framework-json-api/issues/155#issuecomment-301129852>,
> or mute the thread
> <https://github.com/notifications/unsubscribe-auth/AA-sh0T6dTYb3F_sNKgsEZl0Xi1F5STUks5r5I77gaJpZM4Glvcc>
> .
>

@orf I share your views on the matter. This is probably the result of too much coupling between models and serializers. Direct mapping of resources to database models is a convenient but limited approach IMHO. The ability to use serializers to do this kind of mapping is really important.
I agree, that is exactly why serialisers exist and is literally their
function. To bypass that for seemingly no good reason is really annoying,
especially if you want/need to use a json API and this is blocking you.

Receiving no feedback on your merge request and incorrect comments on the
related issues from maintainers is even more annoying.

On 12 May 2017 19:36, "Luca Corti" <notifications@github.com> wrote:

> @orf <https://github.com/orf> I share your views on the matter. This is
> probably the result of too much coupling between models and serializers.
> Direct mapping of resources to database models is a convenient but limited
> approach IMHO. The ability to use serializers to do this kind of mapping is
> really important.
>
> —
> You are receiving this because you were mentioned.
> Reply to this email directly, view it on GitHub
> <https://github.com/django-json-api/django-rest-framework-json-api/issues/155#issuecomment-301153853>,
> or mute the thread
> <https://github.com/notifications/unsubscribe-auth/AA-sh6gSsIyykhmp_SWmb3v9BVGU_BGEks5r5KbKgaJpZM4Glvcc>
> .
>

@orf please remember that maintainers are *fallible* human beings who *volunteer* their time to work on this project. You appear to be frustrated and the best I can do is apologize. I hope you don't take the lack of feedback personally. I believe that all the original maintainers have moved to a mostly inactive role for many months so **all** issues and PRs have seen a low volume of feedback. I picked up the maintainer torch at the end of February and have slowly been working through PR queue to get things going again.

I have extremely limited time for DJA and will prioritize on PR work and work through issues when I can.

As for this actual issue, I'm happy to consider a PR if DJA is blocking something that is possible with vanilla DRF. That sounds like a bug to me as well. If a previous PR was closed because of conflicts, it's only because I don't have the knowledge to resolve the conflicts or time to fix the conflicts myself.

Wearing the new maintainer hat, I'm really looking to open up DJA to all contributors/contributions. That's not to say that the other maintainers didn't, but my focus is on a permissive model where I indirectly help by shaping incoming work from other contributors. I'm most interested in the following goals:

1. Support the JSON API spec to compliance
2. Be as compatible with DRF as possible
3. Have sane defaults to be as easy to pick up as possible
4. Be solid and tested with good coverage (DJA should ideally be a very quiet component in the stack)
5. Be performant.

I hope that motivates you enough to put your work back up for consideration in a new PR. I'd love to talk through some code. 😄 
@mblayman I'm sorry if I am causing a dispute over this while trying to make a technical point.
Your work is much appreciated.

Actually, I think you are right about this being a bug in DRF-JA since DRF has a different behaviour as pointed out by @orf

I'm interested in getting this in as I'm currently working on something which needs this capability, so I'm willing to spend some time on getting a PR together if you would consider merging this.

AFAIU the most updated version of the PR is currently on @rpatterson  fork. Any help in getting up to speed on the original PR would be great. 
I am overjoyed to keep this discussion technical. 😄 

My last comment was to provide an update about what's going on with the maintainers so everyone has context of the project's status. I'm happy to get back on track with the problem reported in this issue.
Thanks for the above suggestions, but how then multiple `lookup_fields` would be handled http://www.django-rest-framework.org/api-guide/generic-views/#creating-custom-mixins ?

see http://discuss.jsonapi.org/t/composite-id-inside-the-resource-object/367/3
Being able to select which field to serialize for the primary key would be very useful. 

The application I'm working on must maintain the original primary keys for backwards compatibility with the old API while we transition to using UUIDs for the new API. We do not want to expose the original integer primary keys at all.

I almost got this to work by extending `JSONRenderer`. However, for relations, I would have to copy & paste the entire `extract_relationships` method because of how the `pk` field is hard-coded.

```py
class CustomJSONRenderer(rest_framework_json_api.renderers.JSONRenderer):
    @classmethod
    def extract_attributes(cls, fields, resource):
        obj = super(CustomJSONRenderer, cls).extract_attributes(fields, resource)
        obj.pop('uuid')
        return obj

    @classmethod
    def build_json_resource_obj(cls, fields, resource, resource_instance, *args, **kwargs):
        obj = super(CustomJSONRenderer, cls).build_json_resource_obj(fields, resource, resource_instance, *args, **kwargs)
        obj['id'] = str(resource_instance.uuid)
        return obj
```
This would also be useful for model-less serializers.
This issue only concerns `ModelSerializer` in case someone wants to use a different id than the primary key.

For a use case where there is no model you can simply use a `Serializer` and it should work.
> This issue only concerns `ModelSerializer` in case someone wants to use a different id than the primary key.

I experienced this issue using the plain `Serializer` and a regular class as the instance. I'd have to go back to confirm but something like this
```python
class Foo:
    def __init__(self):
        # pk instead of id required here if not it barfs
        self.pk = '1'

class FooSerializer(serializers.Serializer):
    id = serializers.CharField(read_only=True)

serializer = FooSerializer(instance=Foo())
```
Am i doing something wrong? 
@mynameistechno [Here](https://github.com/adfinis-sygroup/timed-backend/blob/master/timed/reports/serializers.py) are some serializers which are working. They use a custom queryset but would be the same as when using a python plain object with a pk set.
In case this doesn't work please open a new issue as it should work and doesn't concern this specific issue.

I still experience this issue. "object has no attribute pk' 

Exception on this line here:

https://github.com/django-json-api/django-rest-framework-json-api/blob/9d42d9b7018b08e2df399e598d75f057676b3870/rest_framework_json_api/renderers.py#L489

My object is not an instance of a model. I have an id attribute set on the object however. I am using vanilla Serializer, not ModelSerializer. The only way (that I know) to get around this is to set a pk attribute instead of an id on your instance object.

As far as I can tell the renderer (build_json_resource_obj) assumes a pk is present on the object.
@mynameistechno
Yes this is true and I have also stated this in my https://github.com/django-json-api/django-rest-framework-json-api/issues/155#issuecomment-537432285 above (although it might not have been really clear) that a pk attribute/property needs to be available on plain object to be able to use generic serializer with DJA. This can be a simple read-only property on the object though which simply returns the id.

To be able to configure the attribute/property to be used as id resp. primary key a solution for this issue would need to be implemented.


Ok I'm stuck on this issue. 
I have to implement a django rest_framework with JSON API output with pagination. Seems simple enough. I've done it many times before. But this time I don't have an underlying model to work from.

My data comes from a third-party SDK which just returns a list of dictionaries. It's complex data with nested lists and such, but in the end it's just a list of dicts.

I'm posting my snippets here changing the name to not expose the 3rd party provider.

Here is the serializer I have so far:

```
from rest_framework import serializers

class MySerializer(serializers.Serializer):
    def to_representation(self, instance):
        return instance
```

This is my view:

```
class InstanceViewSet(ModelViewSet):
    queryset = sdk.get_all_the_things()
    serializer_class = MySerializer
    pagination_class = CustomPagination
```
sdk.get_all_the_things() returns a list of dicts, similar to:
```
[{ 
  "foo": "something",
  "bar": "more stuff", 
  ...
 },
 {
  ...
 },


and so on....
]
```

I have no control over this input directly and the underlying dict structure could change at any time. Again the format of the dict isn't all that critical.

This just gives me the dreaded `'dict' objects has no attribute 'pk'`. Because my 'dict' object doesn't have one. So given that django-rest-framework-json-api is requiring one, Is there something I can do in my code to satisfy the 'pk' attribute?
@daveseff 
before you return the objects in `get_all_the_things` you could use `setattr` on each dict of the list calculating the primary key depending on the dict content.

```python
setattr(obj_dict, 'pk', 'value')
``` 
> @daveseff 
> 
> before you return the objects in `get_all_the_things` you could use `setattr` on each dict of the list calculating the primary key depending on the dict content.
> 
> 
> 
> ```python
> 
> setattr(obj_dict, 'pk', 'value')
> 
> ``` 

Yeah I ended up addressing this in a similar fashion. I take the underlying data dict and create an object instance from a simple class definition. The class has a read only pk property method that returns the id.
Thank you kindly @sliverc  and @mynameistechno . I managed to get this working by creating a dummy object and populating a list with those objects. I'm posting my solution here in case any one else comes across this and needs a solution:

```
class DummyObject(object):
    def __init__(self, **kwargs):
        for field in ('id', 'data'):
            setattr(self, field, kwargs.get(field, None))
```

Then had to manipulate the list of dicts. This is somewhat ugly code but worked:
```
resultset = sdk.get_all_the_things()
index = 0
instances = []

for instance in resultset:
    temp_obj = DummyObject()
    temp_obj.data = instance
    temp_obj.pk = index
    instances.append(temp_obj)
    index += 0
return instances
```

Then Changed my serializer to serialize the object data:
```
from . import DummyObject

class MySerializer(serializers.Serializer):
    pk = serializers.IntegerField(read_only=True)
    data = serializers.DictField()

    def create(self, validated_data):
        return DummyObject(pk=None, **validated_data)

    class Meta:
        resource_name = 'instances'
```
If the `fields` parameter of `build_json_resource_obj` method contains an `id` field, use this `id` field, otherwise use the default implementation. I think this is very flexible.

Resolving patch:
diff --git a/rest_framework_json_api/relations.py b/rest_framework_json_api/relations.py
--- a/rest_framework_json_api/relations.py
+++ b/rest_framework_json_api/relations.py
@@ -247,17 +247,21 @@ def to_internal_value(self, data):
         return super().to_internal_value(data["id"])
 
     def to_representation(self, value):
-        if getattr(self, "pk_field", None) is not None:
-            pk = self.pk_field.to_representation(value.pk)
-        else:
-            pk = value.pk
-
+        pk = self.get_resource_id(value)
         resource_type = self.get_resource_type_from_included_serializer()
         if resource_type is None or not self._skip_polymorphic_optimization:
             resource_type = get_resource_type_from_instance(value)
 
         return {"type": resource_type, "id": str(pk)}
 
+    def get_resource_id(self, value):
+        """
+        Get resource id of related field.
+
+        Per default pk of value is returned.
+        """
+        return super().to_representation(value)
+
     def get_resource_type_from_included_serializer(self):
         """
         Check to see it this resource has a different resource_name when
diff --git a/rest_framework_json_api/renderers.py b/rest_framework_json_api/renderers.py
--- a/rest_framework_json_api/renderers.py
+++ b/rest_framework_json_api/renderers.py
@@ -443,10 +443,9 @@ def build_json_resource_obj(
         # Determine type from the instance if the underlying model is polymorphic
         if force_type_resolution:
             resource_name = utils.get_resource_type_from_instance(resource_instance)
-        resource_id = force_str(resource_instance.pk) if resource_instance else None
         resource_data = {
             "type": resource_name,
-            "id": resource_id,
+            "id": utils.get_resource_id(resource_instance, resource),
             "attributes": cls.extract_attributes(fields, resource),
         }
         relationships = cls.extract_relationships(fields, resource, resource_instance)
diff --git a/rest_framework_json_api/utils.py b/rest_framework_json_api/utils.py
--- a/rest_framework_json_api/utils.py
+++ b/rest_framework_json_api/utils.py
@@ -304,6 +304,19 @@ def get_resource_type_from_serializer(serializer):
     )
 
 
+def get_resource_id(resource_instance, resource):
+    """Returns the resource identifier for a given instance (`id` takes priority over `pk`)."""
+    if resource and "id" in resource:
+        return resource["id"] and encoding.force_str(resource["id"]) or None
+    if resource_instance:
+        return (
+            hasattr(resource_instance, "pk")
+            and encoding.force_str(resource_instance.pk)
+            or None
+        )
+    return None
+
+
 def get_included_resources(request, serializer=None):
     """Build a list of included resources."""
     include_resources_param = request.query_params.get("include") if request else None


Test patch:
diff --git a/tests/test_relations.py b/tests/test_relations.py
--- a/tests/test_relations.py
+++ b/tests/test_relations.py
@@ -10,9 +10,10 @@
     HyperlinkedRelatedField,
     SerializerMethodHyperlinkedRelatedField,
 )
+from rest_framework_json_api.serializers import ModelSerializer, ResourceRelatedField
 from rest_framework_json_api.utils import format_link_segment
 from rest_framework_json_api.views import RelationshipView
-from tests.models import BasicModel
+from tests.models import BasicModel, ForeignKeySource, ForeignKeyTarget
 from tests.serializers import (
     ForeignKeySourceSerializer,
     ManyToManySourceReadOnlySerializer,
@@ -46,6 +47,28 @@ def test_serialize(
 
         assert serializer.data["target"] == expected
 
+    def test_get_resource_id(self, foreign_key_target):
+        class CustomResourceRelatedField(ResourceRelatedField):
+            def get_resource_id(self, value):
+                return value.name
+
+        class CustomPkFieldSerializer(ModelSerializer):
+            target = CustomResourceRelatedField(
+                queryset=ForeignKeyTarget.objects, pk_field="name"
+            )
+
+            class Meta:
+                model = ForeignKeySource
+                fields = ("target",)
+
+        serializer = CustomPkFieldSerializer(instance={"target": foreign_key_target})
+        expected = {
+            "type": "ForeignKeyTarget",
+            "id": "Target",
+        }
+
+        assert serializer.data["target"] == expected
+
     @pytest.mark.parametrize(
         "format_type,pluralize_type,resource_type",
         [
diff --git a/tests/test_utils.py b/tests/test_utils.py
--- a/tests/test_utils.py
+++ b/tests/test_utils.py
@@ -14,6 +14,7 @@
     format_resource_type,
     format_value,
     get_related_resource_type,
+    get_resource_id,
     get_resource_name,
     get_resource_type_from_serializer,
     undo_format_field_name,
@@ -392,6 +393,22 @@ class SerializerWithoutResourceName(serializers.Serializer):
     )
 
 
+@pytest.mark.parametrize(
+    "resource_instance, resource, expected",
+    [
+        (None, None, None),
+        (object(), {}, None),
+        (BasicModel(id=5), None, "5"),
+        (BasicModel(id=9), {}, "9"),
+        (None, {"id": 11}, "11"),
+        (object(), {"pk": 11}, None),
+        (BasicModel(id=6), {"id": 11}, "11"),
+    ],
+)
+def test_get_resource_id(resource_instance, resource, expected):
+    assert get_resource_id(resource_instance, resource) == expected
+
+
 @pytest.mark.parametrize(
     "message,pointer,response,result",
     [
diff --git a/tests/test_views.py b/tests/test_views.py
--- a/tests/test_views.py
+++ b/tests/test_views.py
@@ -183,6 +183,50 @@ def test_patch(self, client):
             }
         }
 
+    @pytest.mark.urls(__name__)
+    def test_post_with_missing_id(self, client):
+        data = {
+            "data": {
+                "id": None,
+                "type": "custom",
+                "attributes": {"body": "hello"},
+            }
+        }
+
+        url = reverse("custom")
+
+        response = client.post(url, data=data)
+        assert response.status_code == status.HTTP_200_OK
+        assert response.json() == {
+            "data": {
+                "type": "custom",
+                "id": None,
+                "attributes": {"body": "hello"},
+            }
+        }
+
+    @pytest.mark.urls(__name__)
+    def test_patch_with_custom_id(self, client):
+        data = {
+            "data": {
+                "id": 2_193_102,
+                "type": "custom",
+                "attributes": {"body": "hello"},
+            }
+        }
+
+        url = reverse("custom-id")
+
+        response = client.patch(url, data=data)
+        assert response.status_code == status.HTTP_200_OK
+        assert response.json() == {
+            "data": {
+                "type": "custom",
+                "id": "2176ce",  # get_id() -> hex
+                "attributes": {"body": "hello"},
+            }
+        }
+
 
 # Routing setup
 
@@ -202,6 +246,14 @@ class CustomModelSerializer(serializers.Serializer):
     id = serializers.IntegerField()
 
 
+class CustomIdModelSerializer(serializers.Serializer):
+    id = serializers.SerializerMethodField()
+    body = serializers.CharField()
+
+    def get_id(self, obj):
+        return hex(obj.id)[2:]
+
+
 class CustomAPIView(APIView):
     parser_classes = [JSONParser]
     renderer_classes = [JSONRenderer]
@@ -211,11 +263,26 @@ def patch(self, request, *args, **kwargs):
         serializer = CustomModelSerializer(CustomModel(request.data))
         return Response(status=status.HTTP_200_OK, data=serializer.data)
 
+    def post(self, request, *args, **kwargs):
+        serializer = CustomModelSerializer(request.data)
+        return Response(status=status.HTTP_200_OK, data=serializer.data)
+
+
+class CustomIdAPIView(APIView):
+    parser_classes = [JSONParser]
+    renderer_classes = [JSONRenderer]
+    resource_name = "custom"
+
+    def patch(self, request, *args, **kwargs):
+        serializer = CustomIdModelSerializer(CustomModel(request.data))
+        return Response(status=status.HTTP_200_OK, data=serializer.data)
+
 
 router = SimpleRouter()
 router.register(r"basic_models", BasicModelViewSet, basename="basic-model")
 
 urlpatterns = [
     path("custom", CustomAPIView.as_view(), name="custom"),
+    path("custom-id", CustomIdAPIView.as_view(), name="custom-id"),
 ]
 urlpatterns += router.urls