How can I define a ModelSerializer ChoiceField's choices according to object attribute?

778 Views Asked by At

My Beta model's stage field provides 5 choices. I want my serializer to not always accept all these choices but only some of them according to the serialized object's actual stage value. For example, if my_beta_object.stage == 1, then the serializer should expect (and offer) only stages 2 and 3, if my_beta_object.stage == 2, only stages 2 and 4, etc.

# models.py
class Beta(models.Model):
    class BetaStage(models.IntegerChoices):
        REQUESTED = (1, "has been requested")
        ACCEPTED = (2, "has been accepted")
        REFUSED = (3, "has been refused")
        CORRECTED = (4, "has been corrected")
        COMPLETED = (5, "has been completed")

    stage = models.ChoiceField(choices=self.BetaStage.choices)

# serializers.py
class BetaActionSerializer(serializers.ModelSerializer):
    stage = serializers.ChoiceField(
        # choices=?
    )

    class Meta:
        model = Beta
        fields = ("stage",)

# views.py
class BetaViewSet(viewsets.ModelViewSet):
    serializer_class = BetaSerializer

    def get_serializer_class(self):
        if self.action == "update":
            return BetaActionSerializer
        return self.serializer_class

How can I dynamically limit the choices of that field according to the serialized object's field value?

2

There are 2 best solutions below

1
On

You could try to override the __init__ method of your serializer and then dynamically generates the choices. However, it's not as simple as choices=my_generated_choices. It's a bit more complicated, and there's an in-depth solution on that topic over there

The alternative and simpler method is to use the validate() method, which is triggered at the end of the validation process, after each field has been validated successfully. You could do the following:

  • Write a static dict that maps each status to its valid status choices
  • In the validate method, if updating, check if the new status is a valid choice based on your current status, using your static dict
  • If not, raise a ValidationError
# Example of mapping. Might want to make it pretties and put it in the model itself
mapping = {
  1: [2, 3],
  2: [1, 3],
  # ...
}

# And in validate
def validate(self, data):
    # We're in an update scenario
    if self.instance not None: 
        # It doesnt appear to be required, so use .get()
        new_stage = data.get("stage")
        # Not sure if an instance can have no stage?
        if new_stage is not None and self.instance.stage is not None: 
            # Our check
            if new_stage not in mapping[self.instance.stage]:
                raise ValidationError("Invalid stage")
0
On

Part of the explanation and a start for a solution I found here. By design, I cannot get the context at fields' initialization apparently. I need to redefine the ChoiceField choices attribute at serializer's initialization.

# serializers.py
class BetaActionSerializer(ModelSerializer):
    def __init__(self, *args, **kwargs):
        super(ModelSerializer, self).__init__(*args, **kwargs)
        if self.instance:
            if self.instance.stage == Beta.BetaStage.REQUESTED.value:
                self.fields["stage"].choices = [
                    (Beta.BetaStage.ACCEPTED, Beta.BetaStage.ACCEPTED.name),
                    (Beta.BetaStage.REFUSED, Beta.BetaStage..REFUSED.name),
                ]
            etc.

I'm not marking this answer as a solution because I'm the original poster and I'm not sure this is a solid or desirable solution. However I'm hopeful this could help someone with the same or a similar issue in the future, as I've struggled finding information myself.