Rails Custom Type — as a single value or as an array of custom type values

Kevin Liebholz
Nerd For Tech
Published in
5 min readApr 26, 2021

--

Did you ever need to store some information in the database being of a specific type? Of course, I guess. We all use strings, integers, decimals, …, and, especially in the case when using PostgreSQL also arrays (pg array). But what if we need a type that does not yet exist? Something that is more unique to the app we’re building. Well, Rails got us covered there:

Custom Types

So, let’s have a look at how custom types work!

Intended Results

First, let’s quickly define what we want to achieve. Our Rails example app will have a Payment model which has three attributes:

  • amount
  • currency
  • possible_currencies

So, we’re going hardcore here and don’t only define a “normal” attribute as a custom type, but also, using PostgreSQL, an array of those custom types for the possible_currencies (using pg array). Don’t think too much about it now, we will cover each one individually in a moment. First, we need to run the migration and create the model. We also need to adjust some configurations.

  1. tell the model which attributes are of the custom type
  2. register the type so it can be loaded on the app’s start

First, we start by adding the custom type to the non-array attribute, then we will modify it to only support an array of elements that are all, individually, of the custom type. In the end, we bring both together, so let’s go on!

Currency: a Single Value Attribute of the Custom Type

Describing the Type’s Value

For simplicity, let’s define a currency type in which we store, unsurprisingly, a single currency (I know there is the money gem, it’s just for illustration). But why would we need that? In this case, we want certain behaviors implemented within a currency. We want a currency to be able to show itself in a string format, ISO 4217 code, the symbol (e.g. €) and we also want to be able to parse a currency string back to its currency class and also see all possible currencies. To do this, we describe a module `Currency`and add the single currencies as classes into it where we describe the mentioned methods:

The single classes, e.g. `Currency::EUR` are the values that we want to save to the database and want to get back from the database. So, having a model with attributes of this class, the `Payment`model, we can do

Payment.last.currency.iso
=> ‘EUR’

This means the value we’re getting from the currency attribute must be like

Payment.last.currency
=> Currency::EUR

So it needs to be automatically constantized when we retrieve the value from the database. Let’s see how we can do this.

Creating the Currency Type

For this, Rails gives us the class ‘ActiveRecord::Type::Value’ to inherit from which hands us the 4 methods we need to create our custom type.

  • type: defines how we actually save it in the database.
  • cast: translates the user input to the object. This method gets called, once we assign the currency value (‘Currency::EUR’) to the object’s currency attribute.
  • serialize: translates the value to something the database can understand (in this case a string). This method gets called, once we save the object to the database.
  • deserialize: translates the database understandable string to the currency value when we instantiate a Payment from a database record.

Each of these methods expects a parameter ‘value’. As we never really call any of those methods explicitly (Rails does that for us), we only need to know what these parameters are. And this changes depending on the method. This parameter will be either the value we assign to the object’s attribute (cast), the object attribute’s actual value (serialize), or the string from the database (deserialize).

Within the database, we want the currency to be saved as a string (e.g. ‘eur’), but if we read the value of a Payment object, we want it to be a currency class like ‘Currency::EUR’. So, we can do:

First, we say that we want it to be a string within the database by saying ‘string’ within the type method. If you recall the migration, that’s actually the type of the database column!

On assignment to a Payment object, we give a string to the currency attribute (just like it would be from a form) and it needs to be converted to a currency class. Only once we save the object, the serialize method converts the class to a string (which we defined within the class) and saves it as such. To check that, you could use something like Postico to read your database. If we now want to read that Payment from the database, the string will be deserialized using the `deserialize` method so we can access the currency class again. Try it in irb:

To have a check that this actually works the way we want, we can also add some specs:

As we now have everything needed for the single value custom type, let’s have a look at how it is done for an array.

Currencies: an Array of Custom Type Elements

Now, we want to provide our Payment with an array of currency classes. So that this works:

Payment.last.possible_currencies
=> [Currency::EUR, Currency::USD]

The only thing that needs to be changed here is the currency_type.rb so that it serializes and deserializes the values correctly.

  • cast: we provide an array of currency strings `[‘eur’, ‘usd’] to the attribute which will be translated into currency classes.
  • serialize: on saving to the database, we need to translate this Ruby array to a pg array of strings. Doing this, `to_s` is called upon each element of the array so that it represents the string values of the currency classes.
  • deserialize: when we retrieve the Payment from the database, we need to translate the pg array back to a Ruby array. This will give us an array of strings like `[‘eur’, ‘usd’]`which we then transform to an array of currency classes `[Currency::EUR, Currency::USD]`

Maybe it makes more sense, showing the specs here:

And here goes the type file:

Check it out in irb:

Putting It Together

Now that we have both, non-array and array implementation individually, let’s put it together by starting with the specs:

So basically, it should just do both depending on what you give to it, right? Let’s quickly define the methods:

  • cast: we provide the classes in strings as they would be coming from a form. So, cast needs to decide if it transforms a single currency string into a currency class or an array of currency strings into an array of currency classes.
  • serialize: if it is an array, it needs to convert the Ruby array into a pg array. If, however, it is a single currency class, it simply needs to transform it into a string.
  • deserialize: this is a bit more tricky because we need to check if the value in the database is a pg array. We simply do this by checking the delimiters we find in the PostgreSQL docs. If it is one, we decode it back to a Ruby array and constantize the strings. If not, it is a single currency which we constantize.

So, that’s it. Of course, this cannot only be used for currencies. Just think of spaceship types, and let your imagination flow. I hope this little article was of help to you. Feel free to comment and reach out!

All the best,

Kevin

--

--

Kevin Liebholz
Nerd For Tech

Creator, strategist, and full stack developer. Passionate about using tech to create a sustainable future.