Search code examples
pythondjangoserializationdjango-rest-frameworkapi-design

How do I 'cleanly' create this Django/DRF serialization relationship between models?


I've read that circular imports is a 'code smell' and is fundamentally a bad design choice. I have an app that has models, User, Deck, Hand. I want the User to be able to create a Hand without needing to create a Deck, but also give the User the choice to put the Hand in the Deck if wanted. So I end up with something like this:

(< means ForeignKey Relationship)

User < Deck < Hand

&&

User < Deck

&&

User < Hand

models.py:

class User(AbstractUser):
    pass

class Deck(models.Model):
    created = models.DateTimeField(auto_now_add=True)
    name = models.CharField(max_length=100, unique=True, 
blank=False, null=False)
    user = models.ForeignKey('users.User', related_name='decks', 
on_delete=models.CASCADE, null=False)


class Hand(models.Model):
    created = models.DateTimeField(auto_now_add=True)
    deck = models.ForeignKey('goals.Deck', related_name='hands', on_delete=models.CASCADE, null=True)
    name = models.CharField(max_length=100, blank=False, null=False)
    user = models.ForeignKey('users.User', related_name='hands', on_delete=models.CASCADE, null=False)

serializers.py:

class HandSerializer(serializers.HyperlinkedModelSerializer):
    user = serializers.ReadOnlyField(source='user.username')
    deck = serializers.CharField(required=False, source='deck.name')

    class Meta:
        model = Hand
        fields = ('url', 'id', 'created',
              'deck', 'name', 'user')
        extra_kwargs = {
            'url': {
                'view_name': 'goals:hand-detail',
            }
        }

class DeckSerializer(serializers.HyperlinkedModelSerializer):
    user = serializers.ReadOnlyField(source='user.username')
    hands = HandSerializer(many=True, read_only=True)

    class Meta:
        model = Deck
        fields = ('url', 'id', 'created', 'name', 'user')
        extra_kwargs = {
            'url': {
                'view_name': 'goals:deck-detail',
            }
        }

class UserSerializer(serializers.HyperlinkedModelSerializer):
    decks = DeckSerializer(many=True)
    hands = HandSerializer(many=True)

...

Is this the correct approach API-design-wise in order to achieve what I want app-design-wise? If not, how should I go about doing this? And if so, how do I get around the circular import errors when I change user from a ReadOnlyField to a UserSerializer() field?

Edit:

I was thinking if this approach was bad or impossible with the circular imports, I could create a standard one way relationship like:

User --> Deck --> Hand

and have a default Deck that's hidden from the user so that User can still create a Hand without creating his/her own Deck because it's already been done by default (just hidden away). But this feels like a hack too and I don't know if this approach smells more than the initial.


Solution

  • This is correct. Suppose I want to request a Hand with ID 1. I would do a GET request for /api/hands/1 right? Do I really expect it to serialize a full user with all the hands of that user? Maybe. It just depends.

    To get around it you would define something like:

    MinimalUserSerializer - Returns only email, username, and ID. - Does not return hands.

    And you would use that instead of your full UserSerializer that returns hands all the time.