What do you do when you need to instanciate a class in Rails, but its name is dynamic?
Let’s say we want to serialize an object for use in an API payload and we have to handle different versions.
We could have a MySerializer::V1
and MySerializer::V2
classes.
And to instanciate the correct one, we could do something like this:
serializer_class = "MySerializer::V#{version}".constantize
serializer = serializer_class.new(object)
It works, but we should really avoid it.
It could be unsafe
If the string that is constantized ends up containing user-controlled data, we could be in trouble.
Let’s say you are allowing the user to create different record types based on a type
attribute.
params[:record_type].constantize.create!(...)
Your front-end could only send types like Article
or Category
, but a malicious user could send something like AdminUser
and create themself an admin account.
This is highly unlikely, but it’s still a risk.
It has bad developer experience
- It’s hard to read and hard to follow
- You can’t use go to definition, forcing you to open the file manually
- When searching for a class name, you won’t find the lines with constantize, giving you a false sense of security
How to do instead ?
You could have a whitelist of classes you allow to be instanciated, but this doesn’t solve all problems.
What I like to do is use a hash to map some value with the class you want to call, for instance :
SERIALIZER_CLASSES = {
"v1" => Myserializer::V1,
"v2" => Myserializer::V2
}
def serializer_for(version)
SERIALIZER_CLASSES[version] || raise "Unknown serializer for version #{version}"
end
serializer_for("v2").new(...)
I like it because it’s explicit, easy to read, and allows you to use go to definition or find the line when you search for a class name.