Handling of `value` field accepts different value types

Hi, everyone.

I’m trying to find out the right way to implement “multi-type” field to store different data types as values. I guess the closest example is store’s Product.

class Product(models.Model):
    product_type = ForeignKey(ProductType)
    title = CharField()
    specs = models.ManyToManyField(
        ProductSpec,
        through="ItemSpecValue",
        blank=True
    )
    price = PositiveIntegerField()
    .... some other fields ...

Each of Product types might have different specs like color, size, weight etc.
Most of guides implements a separate model ProductSpec and Product through model ProductSpecValue which has additional value field to maintain flexibility of products parameters.

class ProductSpec(models.Model):
    name = CharField()
    product_type = ForeignKey(ProductType)
    
class ProductSpecValue(models.Model):
    product = models.ForeignKey(Product, on_delete=models.CASCADE)
    spec = models.ForeignKey(ProductSpec, on_delete=models.CASCADE)
    value = # field type?

So the question is: what field type to be set for value field and what is the common approach for such design ? Is there any better one?

Appreciate any help.

Personally I would use a JSONField here and implement a ProductSpecValue.clean method to validate the type of value.

You could also add constraints to ensure your data is of the right type depending on the spec and you’ll get validation for free

class ProductSpec(models.Model):
    name = CharField(unique=True)
    product_type = ForeignKey(ProductType)
    

class JSONTypeOf(models.Func):
    function = "jsonb_typeof"
    output_field = models.TextField()


class ProductSpecValue(models.Model):
    product = models.ForeignKey(Product, on_delete=models.CASCADE)
    spec = models.ForeignKey(
        ProductSpec, on_delete=models.CASCADE, to_field="name"
    )
    value = JSONField()

    class Meta:
        constraints = [
            models.CheckConstraint(
                condition=Case(
                    When(
                        spec="color", then=Exact(
                            JSONTypeOf("value"), Value("string")
                        ),
                    ),
                    When(
                        spec__in=("size", "weigth"), then=Exact(
                            JSONTypeOf("value"), Value("number")
                        ),
                    ),
                )
            )
        ]

Thank you for reply.

That looks reasoning.

And what about JSON field approach to avoid conditional checks with custom Func? :

class ProductType(models.Model):
    name = models.CharField()
    spec_schema = JSONField(
        default=dict
    )

class Product(models.Model):
    product_type = ForeignKey(ProductType)
    title = CharField()
    specs = JSONField(
        default=dict,
        blank=True,
    )
    price = PositiveIntegerField()

    def clean(self):
        schema = self.product_type.spec_schema or {}
        try:
            jsonschema.validate(instance=self.spec, schema=schema)
        except jsonschema.ValidationError as exc:
            raise ValidationError({"specs": f"Schema error: {exc.message}"})

Using a JSON schema will work fine at the Python level but be aware that model validation is not run when using the ORM directly so it you perform any form of backfill (e.g. usage of bulk_create) or call save or create directly your clean method won’t be called.

Looks like there exists solutions on Postgres if you want to enforce JSON schema at the database level as well but they’ll likely require a trigger in your case as CHECK constraints cannot refer other tables fields and in your case the schema lives in a different table.