A team wants to store a product's variable attributes as a JSON column rather than modelling them as tables. When is that right and what is given up?
Show the full answer Hide the answer
What is being tested
Whether you can distinguish data that genuinely has no stable shape from data whose shape is merely inconvenient to model.
When JSON is the right call
- The shape is genuinely per-record and unbounded. A marketplace where each seller defines their own attributes, or a form builder where the customer designs the fields.
- You never query inside it. It is read and written whole, as an opaque blob that the application interprets.
- It is a sparse extension of a well-modelled core. The important attributes are columns; the long tail is JSON.
- You are storing a snapshot of something external — a third-party API response, a webhook payload — where fidelity to what was received matters more than queryability.
What you give up
The type system. A price stored as a string in one record and a number in another is accepted silently. The database will not stop it and you will discover it in a report.
Constraints. No foreign key from a JSON field, no check constraint, no not-null. Every invariant becomes application code, and application code is not the only thing that writes to the table — migrations, admin tools and data fixes bypass it.
The query planner's cooperation. Filtering inside JSON is possible in modern engines and often adequate, but selectivity estimates on JSON paths are much worse, so plans degrade unpredictably as data grows.
Refactorability. Renaming a column is a migration; renaming a JSON key is a data rewrite across every row with no schema to tell you which rows have it.
The failure this usually becomes
The attributes turn out to have a shape after all — 90% of products use the same six keys — and now the product needs to filter and sort by them. You add expression indexes on the JSON paths, then a generated column, and eventually you have reimplemented the schema badly. Meanwhile three years of inconsistent keys and types are in the data.
The recommendation
Model the stable core as columns and use JSON for the genuine tail. If a key appears in most rows, or anything filters or sorts on it, it is a column. Promote keys to columns as they stabilise; this is a cheap, mechanical migration and doing it early is far easier than doing it after three years of drift.
Related anti-pattern worth naming: the entity-attribute-value table, reached for the same reason. It defeats the planner, forbids constraints, and turns every query into a self-join. JSON is strictly better than EAV, and columns are better than both when the shape is stable.