rails 4 before_validation on: :create or on: :save

The before_validation callback allows restrictions to particular events within the lifecycle of an object.

You can use the following syntax:

before_validation :set_uuid, on: :create
before_validation :upload_to_system, on: [:create, :update]

Here is the documentation of this option for Rails 4.0, Rails 5.2, Rails 6.0 and Rails 7.0.


I came across the same issue and here's what I found.

#valid? is a method which also accepts an optional parameter called context (which for some reason a lot of people don't know about, including me before I stumbled upon this question and did some research). Passing in the context when the #valid? method is called will result in only those before_validation callbacks being run for which the same context is set using the :on key.

Example

Let's say we have the following code:

class Model < ActiveRecord::Base
  before_validation :some_method, on: :create
  before_validation :another_method, on: :update
  before_validation :yet_another_method, on: :save
  before_validation :also_another_method, on: :custom
end

Now, calling:

Model.new.valid?(:create)

will only run :some_method. Likewise calling:

Model.new.valid?(:update)
Model.new.valid?(:save)
Model.new.valid?(:custom)

will only run :another_method, :yet_another_method, and :also_another_method respectively. But, if we do:

Model.new.valid?(:unknown)

then it will not call any callbacks because we did not specify :unknown as a context while creating callbacks.

Also, one other thing to note is that if we do not pass a context when calling #valid?, then ActiveRecord will internally use the new_record? method to figure it out. That is, if new_record? returns true, then the context will be set to :create, but if it returns false, then the context will be set to :update.

Coming back to your question

When I put it as on: :save If I try to do an image.save on a test I can see my before_validation callbacks are not ran!

That's because ActiveRecord's #save method internally calls #valid? without passing an explicit context, which means now the #valid? method will have to decide whether to use :create or :update as a context based on the boolean returned by #new_record?. And since you've specified on: :save, it doesn't run. Yes, that's right, :save context doesn't exist internally in ActiveRecord.

If I put on: :create is ran in every situation is does not matter if I ran image.save, image.create or image.valid?

This is true, but only if image is a new record. Try doing the same for an existing record and it will not run.