Skip to content

Environment Variable Coercions

Jeff Felchner edited this page Dec 30, 2020 · 3 revisions

There are many reasons why environment variables are a poor choice for storing your application settings, but one of the most directly developer-facing issues is the fact that environment variables can only be strings.

This means that using environment variables requires gymnastics like this:

if ENV['MY_FEATURE_ENABLED'] == 'true'
  # Do stuff with my feature
end

and imagine if you had to use this variable in a bunch of different places. You end up with a bunch of duplicated and non-ideomatic logic.

The nil / false / Unknown Conundrum

"BUT WAIT!" I hear you say, all you have to do is to say:

unless ENV['MY_FEATURE_ENABLED'].nil?
  # Do stuff with my feature
end

or

if ENV['MY_FEATURE_ENABLED']
  # Do stuff with my feature
end

That seems much better right? Not so fast. This brings me to what I like to call the "nil / false / Unknown Conundrum" (yeah it's a mouthful). The problem with using an unset environment variable to mean "false" is:

  • It could be intented to be nil and not false (which are two different things)
  • It could truly be intented to be false
  • It could have been set to something like the string 'false', in which case the above code would evaluate it to true.
  • It could have been forgotten to have been set altogether (in other words, it should be the string 'true', but we forgot to set it)

Let's look at this with a Chamber example.

The Wrong Way to Deal With Environment Variable Strings

For the purposes of this demostration, we're going to assume we have the following setting.

# settings.yml

my_feature:
  enabled: true

And we're going to assume that we have the following environment variable to override that setting:

export MY_FEATURE_ENABLED="false"
if Chamber.dig!('my_feature', 'enabled')
  # Do stuff with my feature
end

Note: We can't leave MY_FEATURE_ENABLED unset in order to denote false-ness because it could just as easily have not been set because we didn't want to override it in the first place.

Let's look step-by-step at what happens when Chamber loads its settings and processes that code:

  1. Chamber initially reads my_feature.enabled as a Boolean from the YAML file
  2. Then it sees the environment variable and as we stated in the previous guide it understands that we actually want that value instead of what's in the YAML
  3. It sets my_feature.enabled to the String of 'false'
  4. It looks at Chamber.dig!('my_feature', 'enabled') and because in Ruby, all Strings are "truthy", it evaluates it to true instead of the correct value of false

So, what are we to do?

The Right Way to Deal With Environment Variable Strings

Chamber takes a different approach to environment variables. It still reads them in and overrides your YAML settings if they match (this is similar to what dotenv and figaro both do), but then it goes a step further... it converts the String into what the user intended it to be.

I have a confession to make. The Chamber example from the previous section? It works just fine. Let's look at the steps we missed from the previous example (new steps in bold).

  1. Chamber initially reads my_feature.enabled as a Boolean from the YAML file
  2. Then it sees the environment variable and as we stated in the previous guide it understands that we actually want that value instead of what's in the YAML
  3. It sets my_feature.enabled to the String of 'false'
  4. It notices that the original value from the YAML file was a Boolean and converts the String 'false' to the Boolean false
  5. It looks at Chamber.dig!('my_feature', 'enabled') and because in Ruby, all Strings are "truthy", it evaluates it to true instead of the correct value of false
  6. It looks at Chamber.dig!('my_feature', 'enabled') and sees the value of false and correctly evaluates the statement.

Chamber Coercion Types

Chamber will coerce most of the types you care about from environment variable strings. These include:

Type Details
Integer
Float
Array As long as it's a valid YAML array string
Time As long as it's in full ISO8601 format
Boolean Values of yes, no, on, off, true, false, t, f, 1 or 0
NilClass Values of ___nil___ or ___null___ will be treated as literal nil

If any of the conversions error out, or return a type that is not what was expected, an error will be thrown.

Clone this wiki locally