Daniel
Daniel

Reputation: 13

How to apply validation versioning on Rails API models?

I was searching about building Rails APIs and how to right apply some versioning on model validations.

Suppose there is a model and a route like these ones:

class Person < ApplicationRecord
  validates :name, presence: true
end
namespace :v1
  resources :people
end

So, my model is validating if name is present.

But, if I want to release a new version of my API with a new validation on Person to make, for example, a job field mandatory, as this one:

class Person < ApplicationRecord
  validates :name, presence: true
  validates :job, presence: true
end

If I make it this way, I would break my V1 endpoint. So, is there a good practice to make it without breaking my previous V1 endpoint? Or should I drop this validations out of model and make it on request parameter level?

I've saw some gems that makes it at model level but I would like to know if there is some pattern for it without using any additional gem.

Upvotes: 1

Views: 345

Answers (1)

Jakub Kosiński
Jakub Kosiński

Reputation: 907

The simplest way to achieve this is to use :on option on your validations and performing contextual validations, e.g.

class Person < ApplicationRecord
  validates :name, presence: true
  validates :job, presence: true, on: :v2
end

Then you can use person.valid? in your v1 endpoint and person.valid?(:v2) in the v2 endpoint.

But this solution is good enough only for simple scenarios, it's not scalable in long term or more complex apps.

Better solution would be to extract the validation logic to form objects created from your params and have separate form objects for each version of your endpoints that contain some breaking changes, e.g.:

class PersonV1Form
  include ActiveModel::Validations

  attr_accessor :name, :job
  validates :name, presence: true

  def initialize(params = {})
    @name = params[:name]
    @job = params[:job]
  end
end

class PersonV2Form
  include ActiveModel::Validations

  attr_accessor :name, :job
  validates :name, :job, presence: true

  def initialize(params = {})
    @name = params[:name]
    @job = params[:job]
  end
end

Then in your controllers you can just validate form objects before you use them to feed your model:

module V1
  class PeopleController < ApplicationController
    def create
      person = PersonV1Form.new(params[:person])
      if person.valid?
        # ...
      end
    end
  end
end

module V2
  class PeopleController < ApplicationController
    def create
      person = PersonV2Form.new(params[:person])
      if person.valid?
        # ...
      end
    end
  end
end

Upvotes: 3

Related Questions