Search code examples
ruby-on-railsrails-apiapi-versioning

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.


Solution

  • 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