I start to create REST API for my web-application with Django and Django rest framework and I need one logic problem.
There are entities Instruction and Tag. The user visit my service and create self Instruction and add exists Tag OR new Tag for it.
I created my model seriallizer class with using PrimaryKeyRelatedField for relation Instruction<->Tag. But if I do POST for a new Instruction with new Tag I got error: "Invalid pk \"tagname\" - object does not exist.". I solved this problem with the overriding of the to_internal_value method in my field class.
What is the best practice for solving this problem? It seems to me this problem is typical for web and REST API.
My models:
class Tag(Model):
    name = CharField(max_length=32, verbose_name=_("Name"),
                     unique=True, validators=[alphanumeric], primary_key=True)
    def __str__(self):
        return self.name
class Instruction(Model):
    user = ForeignKey(settings.AUTH_USER_MODEL,
                      related_name='instructions',
                      on_delete=CASCADE,
                      blank=False, null=False,
                      verbose_name=_("User"))
    title = CharField(max_length=256,
                      verbose_name=_("Title"),
                      blank=False, null=False)
    created_datetime = DateTimeField(verbose_name=_("Creation time"), editable=False)
    modified_datetime = DateTimeField(
        verbose_name=_("Last modification time"), blank=False, null=False)
    tags = ManyToManyField(Tag,
                           related_name="instructions",
                           verbose_name=_("Tags"))
    class Meta:
        ordering = ['-created_datetime']
        # singular_name = _("")
    def save(self, force_insert=False, force_update=False, using=None,
             update_fields=None):
        n = now()
        if self.id is None:
            self.created_datetime = n
        self.modified_datetime = n
        super(Instruction, self).save(force_insert, force_update, using, update_fields)
    def __str__(self):
        return self.title
my serializers:
class TagSerializer(serializers.ModelSerializer):
    class Meta:
        model = Tag
        fields = ('name',)
class InstructionSerializer(serializers.ModelSerializer):
    tags = PrimaryKeyCreateRelatedField(many=True, queryset=Tag.objects.all())
    author = serializers.SerializerMethodField()
    def get_author(self, obj):
        return obj.user.username
    class Meta:
        model = Instruction
        fields = ('id', 'user', 'title', 'created_datetime', 'modified_datetime', 'tags', 'author')
        read_only_fields = ('modified_datetime',)
I created new field class class PrimaryKeyCreateRelatedField and overrided to_internal_value method for creating the new Tag object instead raising with message 'does_not_exist':
PrimaryKeyCreateRelatedField(serializers.PrimaryKeyRelatedField):
    def to_internal_value(self, data):
        if self.pk_field is not None:
            data = self.pk_field.to_internal_value(data)
        try:
            return self.get_queryset().get(pk=data)
        except ObjectDoesNotExist:
            # self.fail('does_not_exist', pk_value=data)
            return self.get_queryset().create(pk=data)
        except (TypeError, ValueError):
            self.fail('incorrect_type', data_type=type(data).__name__)
my view:
class InstructionViewSet(viewsets.ModelViewSet):
    queryset = Instruction.objects.all()
    serializer_class = InstructionSerializer
    permission_classes = (permissions.IsAuthenticatedOrReadOnly,)
    def create(self, request, *args, **kwargs):
        data = dict.copy(request.data)
        data['user'] = self.request.user.pk
        serializer = InstructionSerializer(data=data)
        if serializer.is_valid():
            serializer.save()
            return Response(serializer.data, status=status.HTTP_201_CREATED)
        return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
Update
models.py
alphanumeric = RegexValidator(r'^[0-9a-zA-Z]*$',
                              _('Only alphanumeric characters are allowed.'))
class Tag(Model):
    name = CharField(max_length=32, verbose_name=_("Name"),
                     unique=True, validators=[alphanumeric], primary_key=True)
    def __str__(self):
        return self.name
class Step(PolymorphicModel):
    instruction = ForeignKey(Instruction,
                             verbose_name=_("Instruction"),
                             related_name='steps',
                             blank=False, null=False,
                             on_delete=CASCADE)
    position = PositiveSmallIntegerField(verbose_name=_("Position"), default=0)
    description = TextField(verbose_name=_("Description"),
                            max_length=2048,
                            blank=False, null=False)
    class Meta:
        verbose_name = _("Step")
        verbose_name_plural = _("Steps")
        ordering = ('position',)
        unique_together = ("instruction", "position")
    def __str__(self):
        return self.description[:100]
class Instruction(Model):
    user = ForeignKey(settings.AUTH_USER_MODEL,
                      related_name='instructions',
                      on_delete=CASCADE,
                      blank=False, null=False,
                      verbose_name=_("User"))
    title = CharField(max_length=256,
                      verbose_name=_("Title"),
                      blank=False, null=False)
    created_datetime = DateTimeField(verbose_name=_("Creation time"), editable=False)
    modified_datetime = DateTimeField(
        verbose_name=_("Last modification time"), blank=False, null=False)
    tags = ManyToManyField(Tag,
                           related_name="instructions",
                           verbose_name=_("Tags"))
    # thumbnail = #TODO: image field
    class Meta:
        ordering = ['-created_datetime']
        # singular_name = _("")
    def save(self, force_insert=False, force_update=False, using=None,
             update_fields=None):
        n = now()
        if self.id is None:
            self.created_datetime = n
        self.modified_datetime = n
        super(Instruction, self).save(force_insert, force_update, using, update_fields)
    def __str__(self):
        return self.title
views.py
class InstructionViewSet(viewsets.ModelViewSet):
    queryset = Instruction.objects.all()
    permission_classes = (permissions.IsAuthenticatedOrReadOnly,)
    def get_serializer_class(self):
        """Return different serializer class for different action."""
        if self.action == 'list':
            return InstructionSerializer
        elif self.action == 'create':
            return InstructionCreateSerializer
serialiers.py
class PrimaryKeyCreateRelatedField(serializers.PrimaryKeyRelatedField):
    def to_internal_value(self, data):
        if self.pk_field is not None:
            data = self.pk_field.to_internal_value(data)
        try:
            return self.get_queryset().get(pk=data)
        except ObjectDoesNotExist:
            # self.fail('does_not_exist', pk_value=data)
            return self.get_queryset().create(pk=data)
        except (TypeError, ValueError):
            self.fail('incorrect_type', data_type=type(data).__name__)
class InstructionCreateSerializer(serializers.ModelSerializer):
    tags = PrimaryKeyCreateRelatedField(many=True, queryset=Tag.objects.all())
    steps = InstructionStepSerializer(many=True)
    user = serializers.HiddenField(default=serializers.CurrentUserDefault())
    class Meta:
        model = Instruction
        fields = ('id', 'user', 'title', 'created_datetime', 'modified_datetime', 'tags', 'steps')
        read_only_fields = ('modified_datetime',)
    def create(self, validated_data):
        tags_data = validated_data.pop('tags')
        steps_data = validated_data.pop('steps')
        # NOTE: tags need add after creation of the Instruction object otherwise we will got exception:
    # "needs to have a value for field "id" before this many-to-many relationship can be used."
        instruction = Instruction.objects.create(**validated_data)
        for tag in tags_data:
            instruction.tags.add(tag)
        for step in steps_data:
            Step.objects.create(instruction=instruction,
                                description=step['description'],
                                position=step['position'])
        return instruction
class InstructionSerializer(serializers.ModelSerializer):
    tags = serializers.StringRelatedField(many=True)
    author = serializers.SerializerMethodField()
    steps = InstructionStepSerializer(many=True)
    def get_author(self, obj):
        return obj.user.username
    class Meta:
        model = Instruction
        fields = ('id', 'user', 'title', 'created_datetime', 'modified_datetime', 'tags', 'author', 'steps')
        read_only_fields = ('modified_datetime',)
 
     
     
     
    