A repository of bitesize articles, tips & tricks
(in both English and French) curated by Mirego’s team.

Validate JSON data in ActiveRecord

ActiveRecord makes it easy to store JSON data inside a record (using a jsonb column in PostgreSQL for example):

create_table "users" do |t|
  t.string "name"
  t.json "profile"
end

class User < ActiveRecord::Base
  # Validations
  validates :name, presence: true
  validates :profile, presence: true
end

In the above model, we make sure name and profile are provided. But what if we want to validate the data inside the profile object.

We could use a custom validation block:

validates :profile, presence: true,
validates_each :profile, do |record, attr, value|
    record.errors.add attr, 'must contain a non-empty list of interests' if !value["interests"] || value["interests"].empty?
  end

But there’s got to be a better way 🤓 This is why, in 2013, we built ActiveRecord::JSONValidator and probably why it’s one of our most popular gem with over 2,000,000 downloads!

Instead of using a custom validation block, we built a custom validator that validates the data against a JSON schema (using the json_schemer gem).

So let’s start with our schema (that we can persist as a .json_schema file):

{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "type": "object",
  "properties": {
    "interests": {
      "type": "array",
      "items": {
        "type": "string"
      },
      "minItems": 1,
      "uniqueItems": true
    }
  },
  "required": ["interests"]
}

We can now get back to our initial model:

class User < ActiveRecord::Base
  # Constants
  PROFILE_JSON_SCHEMA = Rails.root.join('config', 'schemas', 'user_profile.json_schema')

  # Validations
  validates :name, presence: true
  validates :profile, presence: true, json: { schema:  PROFILE_JSON_SCHEMA }
end

user = User.new(name: "Rémi Prévost", profile: { interests: ["foo", "bar"] })
user.valid? #=> true

user = User.new(name: "Rémi Prévost", profile: {})
user.valid? #=> false
user.errors.full_messages # => ["Data root is missing required keys: interests"]

user = User.new(name: "Rémi Prévost", profile: { interests: [] })
user.valid? #=> false
user.errors.full_messages # => ["Data property '/interests' is invalid: error_type=minItems"]

We now have a full validation system using JSON schemas and ActiveRecord::JSONValidator! 🎉