Search code examples
djangorestdjango-rest-frameworkdjango-serializer

DRF Serializer for Writable Nested JSON


Firstly, I would like to mention that I've already seen many SO posts about this issue, but still couldn't figure out the problem occurring here. I would like to POST a JSON object to my Django application, which looks like this:

{
    "id": "1363362773409783808",
    "text": "@MsLaydeeLala Damn haha. Reminds me of my dog. If someone comes into my room while I\u2019m sleeping he gets up and growls at them if he doesn\u2019t recognize them right away.",
    "created_at": "2021-02-21T05:39:49.000Z",
    "author": {
        "id": "112233445566778899",
        "username": "Elrafa559",
        "name": "EL RAFA"
    },
    "keywords": [
        {
            "name": "dog"
        }
    ]
}

and save it to my database, where my models are:

class Keyword(models.Model):
    """
        Represents keywords which we are looking for, must be unique, 20 chars max.
    """
    name = models.CharField(max_length=20, unique=True)

    def __str__(self):
        return self.name


class TwitterAccount(models.Model):
    id = models.CharField(max_length=20, primary_key=True)
    username = models.CharField(max_length=80)
    name = models.CharField(max_length=80)


class Tweet(models.Model):
    id = models.CharField(max_length=20, primary_key=True)
    text = models.CharField(max_length=380)  # 280 for tweet, 100 for links
    created_at = models.DateTimeField()

    author = models.ForeignKey(TwitterAccount, on_delete=models.CASCADE)

    keywords = models.ManyToManyField(Keyword)

    def __str__(self):
        return str(self.author.username) + "/" + str(self.id)


class TweetKeyword(models.Model):
    # TODO is CASCADE the right choice?
    tweet = models.ForeignKey(Tweet, on_delete=models.CASCADE)
    keyword = models.ForeignKey(Keyword, on_delete=models.CASCADE)

for that, I've written this serializer:

class TweetSerializer(serializers.ModelSerializer):
    author = TwitterAccountSerializer()
    keywords = KeywordSerializer(many=True)

    class Meta:
        model = Tweet
        fields = ["id", "text", "created_at", "author", "keywords"]
        # depth = 1

    def create(self, validated_data):
        print(validated_data)
        try:
            author = validated_data.pop('author')
            author, did_create = TwitterAccount.objects.update_or_create(**author)
            print('644444')
            tweet = Tweet.objects.update_or_create(author=author, **validated_data)

            keywords_data = validated_data.pop('keywords')
            for keyword_data in keywords_data:
                Keyword.objects.update_or_create(**keyword_data)

            return tweet

        except Exception as e:
            print("Exception occured in method .create in TweetSerializer")
            print(e.args)
            return None

the error I'm getting in the method create of TweetSerializer is:

("Field 'id' expected a number but got [OrderedDict([('name', 'dog')])].",)
('`create()` did not return an object instance.',)

could anyone explain why the field keywords is getting treated as if it is id?


Solution

  • One way to do what you want is to:

    1. pop keywords data from the validated data
    2. create the tweet object without the keywords
    3. in a for loop create each keyword, and associate it with the tweet object

    Here is the code (I have removed the author part):

    def create(self, validated_data):
        keywords_data = validated_data.pop("keywords")
        tweet = Tweet.objects.update_or_create(**validated_data)[0]
        for keyword_data in keywords_data:
            keyword = Keyword.objects.update_or_create(**keyword_data)[0]
            tweet.keywords.add(keyword)
        return tweet
    

    You can read the DRF documentation about that, there is not that precise example but there are others and a lot of explanation.