[Proposal] Add validated? method to ActiveRecord/ActiveModel

In my last 3 or 4 projects i repeatedly implemented the method: “validated?” which simply defines whether a record has been validated or not. I’ve searched the code and guides but I don’t think I’ve found anything official that does the same thing. So I was wondering if it was something useful and that could be of interest to others. The code is trivial, and could probably be done more cleanly. This is what I add to my ApplicationRecord:

class ApplicationRecord < ActiveRecord::Base
  self.abstract_class = true

  attr_reader :is_validated
  alias_method :validated?, :is_validated
  after_initialize -> { @is_validated = false }
  after_validation -> { @is_validated = true }
end

the method works without any problem even in associated records validated during the validation of the parent, each keeping its own status in its own context.

motivation:
often when you have a model with belongs to and accepts_nested_attributes_for it happens that we do not validate the child with respect to certain conditions, and therefore want to show different concepts in the visualization

I’m willing to propose a pull request if it’s of interest, even if I’m not sure if it’s better to do it in active record or in active model

Thank you for your time

1 Like

In the way you planned to build the PR, what would happen if an attribute was reassigned after validation? Would the object stay validated?

I’m not clear on the motivation. The post says:

often when you have a model with belongs to and accepts_nested_attributes_for it happens that we do not validate the child with respect to certain conditions, and therefore want to show different concepts in the visualization

I don’t really follow any of that. It sounds a bit like you are talking about validates_associated although maybe with a custom validation context? Maybe providing a more concrete example would help explain the use case?

it’s a situation that hasn’t happened to me (to think about it).
I think that conceptually it should follow the logic of the errors object: if you change an attribute with the after_validation callback, the model it belongs to doesn’t do anything automatic on its errors object, it’s something very specific of an application to modify data that has just been validated.

In the standard flow I always imagine it to be:

  1. send attributes from external of ActiveRecord/Model
  2. assign attributes
  3. validate
  4. save
  5. getresponse

a real example I implemented was:

  • I have a model A made up of 3 submodels (B,C,D)
  • model D depends on data from model C
  • model C depends on data from model B

In the view I don’t see the whole form made up of all the models but D when I have the data of C and similarly C with B.

Without the concept of is_validated on the C model I can’t know if it’s been validated(I don’t want to know if it’s valid). Once I know that it has been validated I can continue showing also the part of the form of D

I’m quite a bit confused exactly what your controller is doing. Because it seems like you are talking about multiple web requests to show different parts of the form, and yet you don’t seem to be persisting the “validated” status in any database. How is it possible that you don’t know if a sub-model is validated, didn’t you yourself call .valid? in this same request?

Or perhaps what you’re really trying to find out is if a sub-model is not yet persisted in the database? We already have @model.new_record? and @model.persisted?. Could you use these instead?

yes, you are right, in the example that I made there are multiple requests.

Here is a gist to let me “better” explain:

# frozen_string_literal: true

require "bundler/inline"

gemfile(true) do
  source "https://rubygems.org"

  git_source(:github) { |repo| "https://github.com/#{repo}.git" }

  gem "rails"
  gem "sqlite3"
end

require "active_record"
require "minitest/autorun"
require "logger"

# This connection will do for database-independent bug reports.
ActiveRecord::Base.establish_connection(adapter: "sqlite3", database: ":memory:")
ActiveRecord::Base.logger = Logger.new(STDOUT)

ActiveRecord::Schema.define do
  create_table :projects, force: true do |t|
    t.string :name
  end

  create_table :step1s, force: true do |t|
    t.integer :project_id
    t.string :category
  end

  create_table :step2s, force: true do |t|
    t.integer :project_id
    t.string :product
  end
end

class ApplicationRecord < ActiveRecord::Base
  self.abstract_class = true
  
  attr_reader :is_validated
  alias_method :validated?, :is_validated
  after_initialize -> { @is_validated = false }
  after_validation -> { @is_validated = true }
end

class Project < ApplicationRecord
  has_one :step1
  has_one :step2

  validates_associated :step1, unless: -> { name.blank? }
  validates_associated :step2, if: -> { step1.validated? && step1.valid? }

  validates :name, presence: {allow_blank: false}

  after_initialize -> { build_step1 if step1.nil? }
  after_initialize -> { build_step2 if step2.nil? }

end

class Step1 < ApplicationRecord
  validates :category, presence: true
end

class Step2 < ApplicationRecord
  validates :product, presence: true
end

class Simulation < Minitest::Test

  def test_multi_step

    # first time on the new form of the model with controller/view
    project = Project.new
    assert project.invalid?
    refute project.step1.validated?
    assert project.step1.errors.empty?
    refute project.step2.validated?

    # second time on the new form of the model with controller/view
    project.name = "project name"
    assert project.invalid?
    assert project.step1.validated?
    assert project.step1.errors.any?
    assert project.step2.errors.empty?
    refute project.step2.validated?

    # third time on the new form of the model with controller/view
    assert project.step1.category = "category name"
    assert project.invalid?
    assert project.step1.validated?
    assert project.step1.errors.empty?
    assert project.step2.validated?
    assert project.step2.errors.any?

    project.step2.product = "product name"
    assert project.valid?
    # now we can save
    assert project.save
    
  end
end

no, I don’t want to know if it’s persistent.

I think I understand what you’re trying to do, thank you. Have you considered using validation contexts? Seems like it simplifies the logic, and avoids the need for validated?

class Project < ApplicationRecord
  has_one :step1
  has_one :step2

  validates :name, presence: {allow_blank: false}, on: :pass1
  validates_associated :step1, on: :pass2
  validates_associated :step2, on: :pass3

  # …
end

Controller:

  project = Project.new(project_params)

  if project.valid?(:pass1) && project.valid?(:pass2) && project.valid?(:pass3)
    # save, respond with success
  else
    # respond with project.errors
  end

i tried but the result is that these logics are moved outside the model.
furthermore the context is useful to tell the #save in which context we want to save (therefore incoming to the controller); on the contrary the validated? it would be useful for rendering information outgoing to the client (like forms, or components that will be added to the form with a turbo inject)

if we take your example, we should have a variable in the project form that contains in which context we are saving to perform the various steps and then append conditionals already present in model to know if should or not render the steps with “invalid class” or not.

with the #validated? it is simpler and more immediate during the generation of the form components if the inputs are checked and valid and therefore give correct visual feedback to the user.

these logics are moved outside the model

I would argue that it’s a good thing. Multi-stage validation should not be the default. It should be a special case for this one controller action. At least, it feels much clearer to me. It would be very confusing if model always saved via multi-stage validation, for example if I tried to update it in Rails console. Of course I could be wrong about your particular situation.

the context is useful to tell the #save in which context we want to save (therefore incoming to the controller); on the contrary the validated? it would be useful for rendering information outgoing to the client

In my example, contexts are stages of validation. They should not come from the user, and should not be exposed in the form. Every form submit should always go through all stages: ONLY IF pass1 is valid, we move onto pass2. ONLY IF pass2 is valid, we move onto pass3.

Then, if any pass fails, we know which pass failed. We see it in the errors hash (check the errors in Rails console, you will see it contains context as well). We can use this information to render the failed form differently, depending on which pass failed.

Is that not what you were trying to achieve?

but how do you know that a particular stage had been validated when you’re back in render time again? because the fact that you have an empty “#errors” is not enough to know if it is valid and validated.

Would this suffice?

project = Project.new(project_params)

if project.valid?(:pass1) && project.valid?(:pass2) && project.valid?(:pass3)
  # save, respond with success
else
  valid_passes =
    case project.errors.last.options[:on]
    when :pass1; []
    when :pass2; %i[pass1]
    when :pass3; %i[pass1 pass2]
    end
  # render, tell the view which passes are valid
end

In the above example, if all 3 passes are valid, you know everything was validated and no errors.

If any of the passes failed, you can find out by looking at any error which pass failed.

If you don’t like a case statement, you can make a little private method for validating.

if pass_valid?(:pass1) && pass_valid?(:pass2) && pass_valid?(:pass3)
  # save, respond with success
else
  @valid_passes # already has what you need
end

private

def pass_valid?(pass)
  @valid_passes ||= []
  @valid_passes << pass if @project.valid?(pass)
end

that could be a solution, but it’s not so clean, and it don’t solve the initial problem:

“know if the model has been validated”

starting from 0, on a record you can know:

  • is persisted or not
  • is valid or not
  • it have errors

You have no idea if the record is already validated. And this is for me a usefull information on the model side, and not an information on the controller or view side.

We would also need to support validated?(:context) probably, right?

My personal opinion

I am not a fan of building state in your objects that answers “have I called this method or not?”. Essentially, that’s what validated? would be. “Have I called method valid? or not?”.

The persisted? is different because it doesn’t answer “have I called save or not”, it answers “is this record actually persisted?” Whether it’s loaded from database, a new saved record, etc.

But when it comes to “have I called this method or not?” the best way to know is in the caller.

object.method
# I have _definitely_ called #method. Because I'm here now.

I don’t need to later ask.

object.have_i_called_method?

My argument is: if your caller code doesn’t know whether it called a method earlier, I think it means the caller code needs to be written clearer/better.

And if you have some difficulty carrying that knowledge between different areas in your code, you might be best served adding this to the object yourself, like you did already.

That’s just my opinion on this, and I do personally prefer things like these to stay in the caller. I think a controller action is the right place to manage multi-stage saving of a record, because I don’t want this UX decision to affect how I use console, admin panel, etc.