Off the Rails: Validating non-model classes with…ActiveModel?


Summarized using AI

Off the Rails: Validating non-model classes with…ActiveModel?

Andy Andrea • July 10, 2025 • Philadelphia, PA • Talk

Overview

In the RailsConf 2025 talk "Off the Rails: Validating non-model classes with…ActiveModel?", Andy Andrea discusses strategies and techniques for leveraging Rails' ActiveModel validation system for non-model classes such as Hashes, Structs, or Data instances. The talk is motivated by real-world scenarios where traditional Rails models are not used, including event-sourced applications with custom data layers.

Main Topic

The central theme of the talk is building a single, flexible validator class capable of running Rails validators—both built-in and custom—on arbitrary Ruby objects without the need for creating new ActiveModel classes for every data shape. The approach supports validating Hashes, non-ActiveRecord objects, value objects, and third-party classes without monkey patching.

Key Points

  • Separation of Validation Logic in Rails:

    • Rails validators typically operate on model classes, registering validations at the class level and invoking them at the instance level.
    • There are challenges when the data to be validated isn't a traditional model (e.g., Hashes in event-sourced systems).
  • Initial Proof of Concept:

    • Andrea demonstrates naïvely running the PresenceValidator against a Hash, highlighting immediate issues (missing methods like errors, read_attribute_for_validation, etc.).
  • Creating a Wrapper to Simulate Models:

    • A wrapper class is introduced to handle method missing, provide required interfaces, and wrap arbitrary objects, thus enabling the use of ActiveModel validators on them.
    • SimpleDelegator and method_missing patterns are discussed to reliably delegate method calls or key lookups to the wrapped objects.
  • Handling Validator Options:

    • Required logic for Rails options like allow_nil, allow_blank, if, and unless is implemented by ensuring that validator callbacks and ActiveSupport's callback system are observed.
  • Supporting Conditionals and Dependencies:

    • The system supports conditionals by dynamically providing methods that correspond to keys in the Hash (e.g., supporting dependency in “validates presence of bad_jokes if Aaron”).
  • Dealing with Mutability and State:

    • Addresses the issue of the underlying data mutating while being validated and ensures that the validator always references the current state.
  • Extending Support for Additional Validators:

    • The solution is expanded to delegate all standard validator methods, handle custom validators, and manage edge cases like array validations and confirmation validators.
  • Example Use Cases and Production Anecdote:

    • The primary use case shared is validating event objects in an event-sourced application where data are often Hashes or custom value objects with no model layer.
    • Practical caveats (such as data mutability during validation and validator state isolation) are discussed, with references to test-driven development and lessons from actual deployment.

Main Takeaways

  • It's possible to use Ruby on Rails' powerful validation system outside of models by strategically wrapping any object (like a Hash) to meet ActiveModel expectations.
  • This pattern enables flexible, reusable validation logic across various data shapes, reducing duplication and reliance on heavy model classes.
  • The approach is suitable for event-sourced systems, incoming API validation, and any case where model-less data validation is needed.
  • A reference implementation is available as a gem in the referenced repository, open to community use and extension.

Off the Rails: Validating non-model classes with…ActiveModel?
Andy Andrea • Philadelphia, PA • Talk

Date: July 10, 2025
Published: July 23, 2025
Announced: unknown

Have you ever wished you could run Rails validators to check that a Hash, Struct or Data instance is properly formatted? Have you ever wanted to be able to compose complex validation logic on the fly rather than registering them at a class level with complicated conditionals? Did you ever have a use case for a single, generic validation and thought it’d be overkill to create a new ActiveModel class?

In this talk, we'll explore how to build a single class that’ll accept almost any kind of argument and let you register and run both built-in and custom validations against that argument’s key/value pairs and methods. Through test-driven development and examining the source code for ActiveModel validations and ActiveSupport callbacks, we’ll gradually build a robust solution to support custom and built-in validators and their various options like `if` and `allow_blank`.

Whether you want to validate a JSON field in a model, ensure that an incoming API request is properly formatted, check for valid events in an event-sourced application or run a validator against a class from a third-party library without monkey-patching, this talk will help you use some of Rails’ most classic features in a new and powerful way.

RailsConf 2025

00:00:20.640 as Drew mentioned, my name is Andy Andrea based in Durham, North Carolina. I've been working with Rails
00:00:26.000 professionally for a little over a decade at this point. And in that time, I've used the Rails validation library
00:00:32.800 quite often. Sometimes too often, sometimes not quite often enough. And a
00:00:38.239 couple years ago, uh, I started working on an application that has a pretty customized in-house data access layer.
00:00:44.480 It's an event source application where we have tons of events moving through the system as hashes, as value objects.
00:00:51.280 no active record, no real model layer in the traditional rail sense. And about a
00:00:56.399 year ago, I was tasked with coming up with a way to validate that our events make sense. They're not corrupted. They
00:01:02.800 have all the data, all the shape that they uh need in order for the system to function. And I was told that that
00:01:10.080 validation library should be a single class. We didn't want to have to make new classes for every type of event and
00:01:15.680 then register the validations on all of those classes. And so I thought, well, that isn't very Railsy, but it would be
00:01:23.439 great if I could figure out a way to use the built-in Rails validators because then we don't have to worry about
00:01:29.040 maintaining them. We don't have to worry about documenting them. We don't have to worry about reinventing the wheel and
00:01:34.079 just to get a fraction of the power that we can get from Rails. And so I decided to give it a shot. And it's actually
00:01:40.799 been in production for a little over a year now. So it worked kind of to my surprise. And today I will not be
00:01:47.600 showing you that code, but I will in fact be showing you something that I think is much better. So for anyone that
00:01:54.560 might have snuck in from my work, please don't fire me for that. Uh I am going to assume that everyone
00:02:01.920 has some experience with Rails validators in this talk, but I'm going to take a little bit to just establish
00:02:08.160 some some shared language. And in case anyone is wondering why my slide theme
00:02:13.520 changed from slide to slide, it is because I procrastinated and did not know that there was an official slide
00:02:18.800 theme. So, we're stuck with the first one from Google Slides. Uh, similarly, I'm kind of living life
00:02:24.800 on the edge because I did not change the batteries in my remote since Atlanta, which for those of you who don't know
00:02:30.239 was two years ago. So, we will see how this goes. But, so back to topic with our Rails
00:02:37.360 validators. We kind of have two sides to the same coin. Classes and instances. And at the class level, we have methods
00:02:44.480 that I'm going to say are used for registering validations. These are things like validates, validate,
00:02:50.400 validates with validates presence of, there are quite a quite a good number of these. Um, if we look at our Rails
00:02:56.959 models in Rails 8 and on the flip side, the instance level, we have still a good
00:03:04.480 number of methods, uh, 27, but only four are really relevant to what I think the
00:03:09.840 instance methods often typically do, which is running validations that have already been registered at the class
00:03:16.720 level. Those methods are valid, invalid, validate, invalidate, bang. So again, class level registering your
00:03:23.360 validations, instance level running the validations.
00:03:28.879 And as far as we're concerned, we're just going to be talking about active model today. We do have some validation
00:03:33.920 logic that comes from active record. Typically the stuff dealing with the database, uniqueness, associations, and
00:03:39.200 the like. But in active model, we've got the validations directory, the validations.rb file, the validator.rb.
00:03:47.440 Great thing about being a speaker is sometimes you just pronounce words like validator for no apparent reason. Um the
00:03:53.519 validator is basically where the base classes live. Validations directory is where a lot of the validators like the
00:03:59.680 presence validator, the numeric numericality validator and the like live. Validations.rb is kind of a lot of
00:04:04.959 the helper files bring it all together. And so I mentioned before that I really
00:04:11.599 like and in some cases have probably overused Rails validators. There have
00:04:16.720 been a couple of situations though where I've wished that they work a little bit differently and that makes sense. It's a
00:04:22.560 convention over configuration framework. So those things are I've sometimes
00:04:27.840 wished that I could use them with non-model classes. I've had a hash like in the example I gave earlier where I
00:04:34.160 wanted to validate the key value pairs rather than the columns and the row values uh associated with a particular
00:04:41.199 model. And I've also wanted in certain cases where I've had very complex
00:04:47.199 validation logic with tons of conditionals that says this validation should run or this validation shouldn't
00:04:53.040 run. I would have liked the ability to compose my validations on the fly rather than having them all registered at some
00:04:59.919 global class level in my application. Additionally, I've run into some
00:05:06.080 situations where I've maybe had a param that comes in in a controller that should affect whether or not some
00:05:12.560 validations run or don't run. And since validations are all at the model level, that means you have to get that data
00:05:18.639 into the model. That might mean making a new active model model. That might mean
00:05:24.080 adding some kind of a new attribute or adder accessor to your existing model and putting that in where it's sometimes
00:05:30.240 used but often times not that relevant. And really, there are ways to get around most of these. Most of them involve just
00:05:36.720 making a new class, but sometimes it would have been nice if we didn't necessarily have to go that approach.
00:05:42.720 Sometimes that might feel a little bit heavy-handed to us. And so the question is, do these things
00:05:49.840 matter? I would say no, not really. But since that's really bad marketing for this
00:05:56.320 talk, I'll say please don't leave just because I said that. And so in this talk, we are going to assume that these
00:06:02.639 things are at least worth considering and worth investigating. And so our goal by the end of this talk
00:06:09.520 is to have a validator class that can run Rails validators against any kind of
00:06:14.639 input, whether it's a hash, whether it's a model. We're going to do this without any monkey patching, without any kind of
00:06:20.080 type casting or coercion. We're also going to have it have its own independable or independent state at the
00:06:27.039 instance level. So nothing global, nothing dependent upon any other validations that might have been
00:06:32.240 registered in your system. And we're also not going to care about the shape or the structure of that data. If you
00:06:38.240 have hashes in array, it's going to work. If you have hashes that are deeply nested, it's going to work. If you've got a mixture of hashes and models,
00:06:45.440 it'll all work. So to get started on that, we're going to start off with a fairly simple proof
00:06:51.280 of concept where we're just going to say we want to run the Rails presence validator against a hash.
00:06:58.800 And so to get started, we can look at the source code for presence validator. And it's actually really simple. This
00:07:04.479 isn't the whole thing, but it's most of the logic in there. There's some extra comments, a little helper method, but
00:07:09.759 this is the entire validate each method. And this is really the only public method that we see on presence validator
00:07:15.360 when we look at the source. And it's pretty simple. It takes in a record which is typically going to be an active
00:07:20.800 record model in our typical use cases, but now is going to be a hash. Adder name that would typically be a column on
00:07:27.199 a table is now going to be a key in that hash. And the value is going to be the value associated with that key. And so
00:07:34.000 it's very simple. We add an error if the value is blank. So we're going to start off with something very, very naive.
00:07:40.800 create a simple validator class that takes in our object to validate our hash and the constructor. We've defined a
00:07:47.280 validates presence of method that takes in some keys. This would be like validates presence of first name, last
00:07:52.960 name, first name, last name are the keys and accept our normal options that we would normally pass into Rails. So we
00:07:58.960 can just instantiate our presence validator. We do have to pass it those keys uh as the arguments to the
00:08:04.639 constructor. iterate over the keys, grab the value, and then call that validate
00:08:09.840 each method that we saw. And like I said, this is very, very naive. It's very, very simple. And because of that,
00:08:16.240 you might be very pleasantly surprised to know that this doesn't actually work.
00:08:22.000 If we call this little proof of concept where we want to say that we're validating the presence of presents, maybe it's a Christmas themed app or
00:08:28.400 something, then it goes boom. But it doesn't really have too many issues.
00:08:34.399 There are only four errors that we have to solve before this runs. Rather than
00:08:39.440 going through them one by one, I'm just going to show them all off. So, first off, we get undefined method errors. If
00:08:45.440 we think back to that presence validator source code that was doing record.sadd,
00:08:50.560 our record is our hash. It doesn't have an errors method. This makes sense. That's something we'll have to fix. If
00:08:55.680 we fix that up, we'll see that we've now left the presence validator behind. And when we're building up the errors
00:09:01.839 themselves, we see no method for read attribute for validation.
00:09:07.360 Similarly, undefined method model name and human attribute name. So currently, all of these methods are
00:09:14.320 being called on our hash, our record. And so we need what we're passing into the presence validator to define all of
00:09:20.800 these methods. And like I said before, we don't want to do monkey patching. We don't want to add read attribute for
00:09:26.240 validations to every single hash in Ruby. So to fix that up, we can create another
00:09:32.959 little class that I'm going to call the wrapper class. And now you're going to see the word new a lot in the next
00:09:38.880 couple of slides. It's going to be weird, but just bear with me. So we can define a new method on wrapper. You can
00:09:44.480 call to get a new anonymous class. And then you can call new on that anonymous
00:09:50.720 class to get an instance of that class. And within this anonymous class, we're going to define our constructor. It's
00:09:55.920 going to take in that hash. We're going to set up the errors with an adder reader going along with the instance variable so that we have that errors
00:10:02.880 method defined. And then we're also going to define read attribute for validation that second myth missing
00:10:08.480 method to just grab the value associated with the attribute that we're trying to validate. So this is pretty simple. We
00:10:15.440 do have those two other errors that are still in there and we can get those by just extending some built-in uh Rails
00:10:21.279 modules naming and translation which also means that we're kind of on the path to get some internationalization in place which is always great.
00:10:28.800 And so then now if we go to our validator and we uh set it up so that
00:10:36.399 instead of passing in the hash to the validate each method as that first
00:10:41.519 record argument, we now wrap the hash in our new instance of that anonymous class
00:10:47.600 that wrapper. Then we can go back run our tests again
00:10:54.399 and it passes. And so now we have successfully validated the presence of
00:11:00.240 presence on a hash. And it's very simple. We just pass the hash in to the constructor. We call validates presence
00:11:06.320 of and it works. And this is pretty cool. Honestly, this didn't take as long as I expected it to. But I'm not going
00:11:13.600 to call our proof of concept done quite yet. And that's because we're not supporting all of the options that we
00:11:20.160 would normally have access to in our presence validation. So, first off, if
00:11:25.519 we look at allow nil, this holds true for allow blank, but I don't really know why you would ever use allow blank on a
00:11:30.640 presence validator. Um, we can check to see if rank is nil. And we're saying we're going to allow a nil rank, we
00:11:38.959 still get the error. Rank can't be blank. And so this means that something is going wrong. Something about allow
00:11:44.800 nil isn't being taken into account. So, if we go back to our presence validator, we saw that it inherited from each
00:11:50.880 validator. And if we go to each validator, we actually see the code that handles these
00:11:56.000 options directly in each validator's validate method. Uh underneath this a little bit, it calls validate each. So
00:12:02.720 that tells us, okay, we're just bypassing the code we need. So if we go back to our validator, no longer iterate
00:12:08.880 over our keys, just pass them into the constructor as we were before, and now call the validate method, passing in
00:12:15.120 again our wrapped hash. It works. Pretty simple.
00:12:20.560 But but but but we have one more thing left before I'm going to call our proof of concept done. And that is supporting
00:12:27.680 if noneless. So here we're saying validates the presence of speaker if false. So it's basically saying never uh
00:12:35.440 never run this. But we still get the error speaker can't be blank. So now to
00:12:41.440 fix this again, we go back to our presence validator. This is kind of where we start whenever we're debugging
00:12:46.560 this kind of stuff. I mentioned there was a bunch of comments that I wasn't showing on that first slide. So if we go
00:12:52.000 back, we actually see in the comments, it tells us directly where to go. Go to class methods validates.
00:12:58.240 So if we pull up this method in the Rails source code, and this is the method that we're probably really familiar with from our active record
00:13:04.959 models, if we're ever doing a validates first name, presence true, it's that
00:13:10.480 validates method that we're looking at right here. And so within validates that then calls validates with which we might
00:13:16.959 know from running custom validators. Validates with then calls validate. Validate then calls set call back. And
00:13:23.200 this is pretty cool. This is where we've actually left active model behind entirely. And now we're in active
00:13:28.959 support callbacks because at the end of the day we do have a lot of validation logic in Rails but a lot of the
00:13:34.959 validation paradigm is basically just two callbacks in a trench code. And so if we look at callbacks, we see
00:13:43.120 we've got our options here if and unless. And now earlier we're just calling the validate the validator
00:13:49.360 directly. We're completely bypassing that paradigm I mentioned earlier of registration and then running. And so
00:13:55.440 this tells us that we need to hook into that callback code to get these options working. And so if we make a little
00:14:02.480 change to our validator where we now just delegate a bunch of things away to
00:14:07.680 our wrapper and our wrapper class. And now we're delegating those class methods that we're used to from our active
00:14:13.760 record models to our validation wrapper class, that anonymous class that's being returned by wrapper. And then we're
00:14:20.880 delegating the instance variables to the instance of that class. And then meanwhile on the wrapper side of things,
00:14:28.079 we have a very small little update that we make which is just include active model validations. This is great. We've
00:14:34.560 got access to all of our validator methods and logic that we're used to at this point. And so then now if we go
00:14:41.519 back and check a simple little if and unless then we get the behavior that we'd
00:14:46.880 expect. And now we're also having to change things a little bit where we're first registering that validation with
00:14:52.720 the validates presence of method and then calling it with valid question mark.
00:14:58.399 Now we're so so close and I've said this a million times but we are very close to having the proof of concept. There's one
00:15:05.199 thing left and that's dependent validations. So this is where if you would have validates the presence of
00:15:11.040 first name if last name and so if we do a simple little test here where we want
00:15:17.279 to validate the presence of bad jokes if Aaron which you know prevents probably a
00:15:22.480 pretty big bug because that wouldn't make sense. No bad jokes. Um then we can
00:15:27.519 run this code and it blows up undefined method Aaron. All the work the guy's
00:15:33.519 done and no method named after him. And so here this kind of makes sense because under the hood we're basically still
00:15:40.079 calling methods on our underlying hash object. And since the hash doesn't have
00:15:45.760 methods that correspond to its keys, we don't have that method to call when
00:15:50.959 we're using the if or the unless. And so in order to have those methods defined,
00:15:56.160 we need to basically convert our hash from key value pairs into method return value pairs. And we've got a lot of ways
00:16:03.279 that we can do this in Ruby. uh method missing a bunch of different delegator patterns. I'm just going to choose to
00:16:08.560 use simple delegator because I like it and I don't think it's used enough so I want to use it here. So here we change
00:16:15.440 our new anonymous wrapper classes to inherit from simple delegator in the
00:16:20.639 constructor we now wrap our hash in a data object. And so where we once had
00:16:26.160 key value pairs, now we have basically a value object whose methods are the keys
00:16:31.279 from our hash and whose return values are the values from our hash. And then we call super so that any methods that
00:16:37.279 aren't defined on our anonymous class just get forwarded along to our new data object.
00:16:43.680 So if we go back to our test, validate the presence of bad jokes. If Aaron, we get bad jokes can't be blank, which I
00:16:51.120 think is a much better error message than anything I've actually written in production code. Uh but yeah, and this
00:16:57.040 is this is great because I mean, if you think back to the applications that you maintain and you imagine the bug that
00:17:02.399 would be on the level of severity as an Aaron Patterson talk that didn't have bad jokes, you would know that we're
00:17:07.600 really probably saving a lot of people a lot of money right now. The small laughter is the best laughter.
00:17:16.799 All right, so now I'll say our proof of concept is finished, but we do have a couple of other things that we probably
00:17:22.959 want to support, namely all of our other validators. And this is super easy with our current setup because it's just a
00:17:29.280 bunch more delegate calls. We're just delegating all of these Rails methods to
00:17:34.320 our little anonymous class that has that active model validations module in it.
00:17:39.679 And the one thing that might be a little bit less familiar to everyone here is this clear validators bang method. This
00:17:45.760 does exist on all of your active record models. You could call this in your
00:17:50.799 application code so that suddenly in the middle of your app running none of your validations exist anymore. I don't think
00:17:58.559 that makes sense to do in production personally. But here where you have this
00:18:03.840 more isolated state, it might make a little bit more sense to say, "Oh, you know, I've set some things up, but now I
00:18:09.679 just want to clear them all out because some weird condition has been met."
00:18:14.799 The other thing that you will notice if you look at the tests for this, which I'll cover the tests a little bit later,
00:18:21.120 is that we do have a little bit of a bug. And that bug is that if our
00:18:26.240 underlying hash that we're validating changes or mutates after our validator
00:18:31.520 has already been instantiated, particularly after that wrapper has already been instantiated, suddenly the
00:18:36.720 wrapper just has access to the old state because that was that's what was used to instantiate that value object, that data
00:18:43.200 object. But meanwhile, the hash has changed. So it might say, "Hey, bad
00:18:48.240 jokes is blank when bad jokes isn't blank on the actual hash." And so we do
00:18:54.559 have to get rid of our simple delegator our IP. It did not last long. And we're just going to hold on to our object to
00:19:00.720 validate that hash again in our instance variable in our constructor. We can bring back our read attribute for
00:19:06.480 validation. That's just how it looked before. And now we'll handle that delegation through method missing. And
00:19:12.400 so if the method is missing, check to see if it's a key on the hash. If so, return the value. Otherwise, do what you
00:19:19.280 normally do. And the thing that's nice about the read attribute for validation pattern here is that because it's not
00:19:26.080 just relying on methods being defined on our wrapper, this also means that you
00:19:31.679 can actually validate things that match methods on this class, which isn't much.
00:19:37.360 But if you wanted to say validate the presence of the errors in your hash, you can do this now because it'll actually
00:19:43.520 be looking at the key value pair and not the errors method on the wrapper. This
00:19:48.640 also means that you could do something like validating the presence of the nil question mark key value pair, but I
00:19:53.679 don't know that there's many use cases for that. All right, and those are kind of the the
00:20:00.000 big the big things for the validator, but there are a bunch of other things that we could spruce up at some bells
00:20:06.880 and whistles. So, first off, if our underlying hash doesn't actually have
00:20:12.400 the key defined on it that we're trying to validate, it will currently blow up because if we were looking back at that
00:20:19.280 uh if object validate key else super, the super will just call no method
00:20:25.360 error. That might not be what we want to do if we're validating say an incoming API request. We're expecting it to have
00:20:32.000 some particular key value pairs defined on it. So maybe we would want to treat that as like a nil value rather than a
00:20:38.480 no method error. And if you follow the link, I'll put these all in the Slack
00:20:43.679 channel. Then you can see some code to handle this. Next up, all of our code so
00:20:48.720 far is just assuming we're only passing in a hash. If you wanted to pass in a model or some other class, maybe that
00:20:55.440 comes from a gem, then this commit will do that for you. Now, this one is a
00:21:02.000 little bit funny. So if we think back to our validator, we are exposing the
00:21:07.600 typical Rails class methods as well as the typical Rails instance methods all
00:21:12.960 as instance methods on our validator class. And in Rails, we have a validate
00:21:18.480 method on both the instance and class level. Validate on the instance level runs the validations. Validate on the
00:21:25.600 class level is what you would use to run a custom method validation on your
00:21:30.640 instances. And since we can't have multiple methods with the same name, I had to choose one to call validate. I
00:21:36.799 chose the instance level one. But you probably will want the behavior that validate the class level validate gives
00:21:43.120 you. And so basically just create a little alias for it. And then you're all good.
00:21:49.280 Custom validator classes are completely supported out of the box. We don't actually have to change anything about our current code to support your custom
00:21:56.400 validator classes. But I do have an example in the repo that shows how you could actually set up a custom validator
00:22:03.120 to run validations against arrays. So if you have an array of hashes and you
00:22:09.039 wanted to validate that all of them have particular key value pairs, for example, you can do that with the linked custom
00:22:16.080 validator. It's a little bit weird. It's a little hacky, but hopefully a decent proof of concept.
00:22:22.799 Now the confirmation validator is kind of a weird one. I did not know this until I got some failing tests, but the
00:22:29.760 confirmation validator actually does a little bit of meta programming with the thing that you're validating sometimes,
00:22:36.960 maybe sort of, and that can cause some breaking tests um if you're passing in a
00:22:44.480 hash. And so this is probably the hackiest thing that's going to be in this entire bit of code that I put
00:22:50.320 together to support this, but it's kind of hacking around something that feels
00:22:55.919 like a little bit more hacky than normal to begin with. And last, but certainly not least, I
00:23:02.720 really love should match. Shout out to ThoughtBot. I use these for testing all of my validations, all of my
00:23:07.919 associations. And so I wanted to see if I could make them work with this pattern. And again, had to do something
00:23:14.720 a little bit hacky, but did actually make it work and it worked pretty well as far as I could tell. Um, so I've got
00:23:20.960 a link to a little support file that I put together as well as the example specs that anyone can look at later on.
00:23:29.039 So this is the repo. Again, I'll put all these slides in Slack. And if you look at this repo, you'll see a bunch of
00:23:35.200 commits, a bunch of branches. Every commit basically solves one problem, one that we either talked about today or
00:23:40.960 added some kind of new feature like the ones that I didn't get to talk about too much uh just a minute ago. All of the
00:23:46.960 branches are basically different checkpoints um like the proof of concept for example. And that is basically it.
00:23:55.520 It's set up as a gem. It's not pushed anywhere. If anyone wants to push it anywhere, feel free to fork it and push
00:24:01.039 it up to Ruby gems. But otherwise, uh, as classic talks go, I did go a lot
00:24:07.919 faster talking than I did in prep. So, if anyone has any questions, uh, you can
00:24:14.400 feel free to shout them out now or come up to me after.
00:24:25.120 Uh, the question, thank you for the reminder. The question was, could I give an example of when a hash might change
00:24:31.440 after instantiating the validator? Is that right? Or after the wrapper. Yeah. So in
00:24:37.600 shoulda matchers when you first uh set up your your subject which would
00:24:42.960 normally be your model your like describe class new it first will test
00:24:49.120 with I think I don't know if it actually does the happy path or the unhappy path first but basically it will first take
00:24:56.880 the subject that you've set up in your RSpec test say I've set my bad jokes to
00:25:01.919 false and then if I'm validating the presence of bad jokes or maybe in this
00:25:07.679 case the absence of bad jokes. Then once it's validated, okay, this doesn't raise
00:25:12.799 an error when it's absent, then it actually goes and changes the underlying object. In your active record model, it
00:25:18.720 would be like model.bad jokes equals true. Um, so in that case, all of the
00:25:24.320 should matcher tests were failing until I swapped out the delegator pattern to
00:25:29.440 actually keep track of the underlying hash. And so I don't think in real
00:25:35.440 production application code you're probably likely to see that happen. But
00:25:40.559 it is good to know if you're in the habit of like duping something that you pass around and then you maybe have some
00:25:47.440 code that will modify it in place later and the validator only has access to the dupe and not the original then it might
00:25:53.520 get a little out of sync. Any other questions? Yes.
00:25:59.679 What was your use case? Yeah. Uh the use case for using this
00:26:05.120 validator um so we have an event sourced system at work and so we have a lot of
00:26:12.320 data that starts off as a hash and because we don't use active record or any kind of real OM we either just have
00:26:20.240 it as a hash or later on as like kind of like a custom value object similar to the Ruby data class. And so because of
00:26:27.919 that and because of the requirement that I had that we didn't want to be setting up a new class for every different type
00:26:34.559 of event in the system, I needed some way of just passing in an event, a hash
00:26:40.880 event into some object that could then run the validators against it. And so
00:26:46.320 since we didn't have a model where we could just throw in the typical Rails pattern of validates this, validates
00:26:51.840 that, had to figure out some new solution.
00:26:57.120 Yes. Um was were both the the readers and the writers essentially of the event
00:27:03.520 using this this pattern um before it was sent over the wire. So the way that it currently works is that the source of
00:27:10.880 the events can kind of change. We have a number of different events in the system. Uh the ones that are most
00:27:17.039 relevant to this particular pattern we call user sourced events. And this is like something that starts off as params
00:27:24.159 coming into a Rails controller for example and they just come in get put in
00:27:30.159 like a little hash similar to the params object and then when they go into kind of the beginning of our events sourced
00:27:37.360 pipeline where we stuck the validation logic eventually they get like written to a database and whatnot. Um when they
00:27:44.400 first come in as a hash that's where we validated them. We don't currently have
00:27:49.600 any validation happening after they've already been written. Um, so far I don't
00:27:55.440 think we've seen the need for that. It was mostly the case of with all of our
00:28:00.640 events happening, they were naturally ordered based on when they occurred. And so if we had some event that we later
00:28:07.919 found out was invalid or was broken, then fixing that past data could mean
00:28:15.120 that we would have some ramifications for the data that's entered the system since then. So it got kind of complex
00:28:20.480 and just saying, okay, before we actually persist this event in our event stream, we want to make sure it's valid,
00:28:27.200 otherwise we're going to discard it. discarding it then kind of triggers potentially down to like the UI raising
00:28:32.399 like a little toast that says, you know, you didn't enter in the first name when you needed to or something.
00:28:39.360 Um, are you asking where we store the the events? No, the validator
00:28:45.760 uh like the class that I showed. So when you do validate
00:28:52.880 Oh yes yes yes yes. So that uh the question is where are the validations
00:28:58.000 that we've registered kind of stored within the system. Is that right? Yeah. So that just leverages what Rails does
00:29:04.799 by default which we get from that include active model validations. And so under the hood um when you include
00:29:11.440 active model validations there's a class attribute called underscore validators
00:29:17.039 that get set up. And so when you do something like validates presence of it basically appends to that underscore
00:29:23.919 validators at the class level. And the way that the class attribute is set up
00:29:29.120 is that it can just be if you have like a series of of class an ancestry like
00:29:35.120 model inherits from other model inherits from other model. Then if I remember correctly the subclass has kind of its
00:29:42.480 own version of the class attribute that pulls in the parent. And so if you've ever had like a single table inheritance
00:29:50.880 active record model setup where you have a base class that has some validators defined and then you have a subclass
00:29:56.960 that adds more that subclass will have not only the ones defined on itself but the the superass as well. And so long
00:30:05.440 story short underscore validators uh it's a method that you can call on your active record models on the class level
00:30:11.760 if you ever want to see all the ones that have been set up. You can also look at uh I think it might just be the
00:30:17.679 callbacks method and that kind of shows you how all of the validators tie back
00:30:22.720 to just call backs that are set on your models as well.
00:30:28.080 Let's see. And I am out of time. So, thank you all again for coming. If anyone has any more questions, feel free
00:30:33.520 to flag me down.
Explore all talks recorded at RailsConf 2025
Ben Sheldon
Sam Poder
Rhiannon Payne
Joe Masilotti
Josh Puetz
Wade Winningham
Irina Nazarova
Tess Griffin
+77