Question

Convert Ruby Hash String Keys to Symbols

rubyhashmap

Question

What is the cleanest and most straightforward way to convert all string keys in a Ruby hash to symbol keys, especially after loading data from YAML?

my_hash = YAML.load_file("config.yml")

I want to access a value like this:

my_hash[:key]

instead of this:

my_hash["key"]

Short Answer

You will learn why Ruby treats string and symbol hash keys as different keys, how to convert top-level keys with transform_keys, and how to recursively symbolize keys in nested YAML data. You will also see the Rails shortcut and when keeping string keys is the better choice.

Concept

A Ruby Hash stores values under keys. A key can be many object types, including a String or a Symbol.

Although these look similar, they are not the same key:

settings = { "theme" => "dark" }

settings["theme"] # => "dark"
settings[:theme]   # => nil

YAML mappings commonly become hashes with string keys because YAML keys such as theme: or "theme": are parsed as strings in many configurations. If your Ruby code expects symbol keys, you must either convert the keys or consistently use strings.

Ruby provides Hash#transform_keys for creating a new hash with transformed keys. For nested configuration data, a recursive conversion is necessary because transform_keys changes only the current hash level.

This matters because inconsistent key types cause subtle bugs: a hash may contain a value, but a lookup returns nil because the code used the other key type.

Mental Model

Think of a hash as a set of labeled drawers.

  • The drawer labeled "key" has a string label.
  • The drawer labeled :key has a symbol label.

Even though both labels display the word key, Ruby considers them different labels. Asking for :key does not open the drawer labeled "key".

Converting keys is like replacing every string label with a symbol label so that your application uses one consistent labeling system.

Take Quiz

Syntax and Examples

Use transform_keys to convert the keys at one hash level:

string_keyed = {
  "host" => "localhost",
  "port" => 3000
}

symbol_keyed = string_keyed.transform_keys(&:to_sym)

symbol_keyed[:host] # => "localhost"
symbol_keyed[:port] # => 3000

transform_keys does not modify string_keyed; it returns a new hash.

string_keyed["host"] # => "localhost"
string_keyed[:host]   # => nil

To replace the contents of the existing hash, use transform_keys!:

string_keyed.transform_keys!(&:to_sym)

string_keyed[:host] # => "localhost"

For YAML with nested hashes, use a helper that walks through hashes and arrays:

def symbolize_keys(value)
  case value
  when Hash
    value.each_with_object({}) do |(key, item), result|
      symbol_key = key.is_a?(String) ? key.to_sym : key
      result[symbol_key] = symbolize_keys(item)
    end
  when Array
    value.map { |item| symbolize_keys(item) }
  else
    value
  end
end

Then load and convert the YAML data:

require "yaml"

config = YAML.load_file("config.yml")
config = symbolize_keys(config)

config[:database][:host]

Step by Step Execution

Consider this nested hash:

input = {
  "database" => {
    "host" => "localhost"
  },
  "features" => [
    { "name" => "search" }
  ]
}

output = symbolize_keys(input)

The recursive helper processes it in this order:

  1. input is a Hash, so it creates a new empty result hash.
  2. It sees the key "database", converts it to :database, and recursively processes its value.
  3. The database value is another Hash. Its "host" key becomes :host; its string value stays "localhost".
  4. It sees the key "features", converts it to :features, and recursively processes its value.
  5. The features value is an Array, so Ruby maps over every item in it.
  6. The array item is a hash, so its "name" key becomes :name.
  7. The final result is:
{
  database: { host: "localhost" },
  features: [{ name: "search" }]
}

Now all of these lookups work:

output[:database][:host]       # => "localhost"
output[:features][0][:name]    # => "search"

Real World Use Cases

  • Application configuration: Convert parsed YAML settings so application code can use config[:database][:host].
  • Background-job settings: Read retry, queue, and timeout values from a configuration file with a consistent key style.
  • API normalization: Convert keys after parsing external data when the rest of an application uses symbol-keyed hashes.
  • Test fixtures: Normalize fixture data before passing it to service objects that expect symbols.
  • Command-line tools: Load a YAML options file and validate fields through symbol-keyed lookups.

When data comes from JSON, its object keys are inherently strings. In that situation, either normalize the data at one boundary or design the rest of the code to use string keys consistently.

Take Quiz

Real Codebase Usage

A common project pattern is to normalize external data at the boundary where it enters the application:

raw_config = YAML.load_file("config.yml")
config = symbolize_keys(raw_config)

After that point, internal code can reliably use symbol keys. This avoids repeatedly checking both forms:

# Avoid spreading this throughout the code:
config[:host] || config["host"]

In Rails, Hash#symbolize_keys is commonly used for ordinary hashes:

options = { "page" => 2, "per_page" => 25 }
symbol_options = options.symbolize_keys

Rails also offers deep_symbolize_keys for nested hashes:

options.deep_symbolize_keys

Use a clear convention within a codebase:

  • Use symbol keys for Ruby-owned options and internal configuration.
  • Keep string keys when working directly with JSON-like payloads, user-provided arbitrary keys, or APIs that require exact string names.
  • Normalize once near the input boundary rather than converting repeatedly.

Common Mistakes

Expecting strings and symbols to match

hash = { "key" => "value" }
hash[:key] # => nil

Use the same key type used to create the hash, or convert the keys first.

Converting only the top level of nested YAML

config = {
  "database" => { "host" => "localhost" }
}.transform_keys(&:to_sym)

config[:database][:host] # => nil

Only "database" was converted. Use a recursive helper for nested structures.

Calling to_sym on keys that are not strings

This can fail if a parsed hash has integer, boolean, or already-symbol keys:

{ 1 => "one" }.transform_keys(&:to_sym)
# NoMethodError: undefined method `to_sym' for 1:Integer

Convert only strings:

hash.transform_keys { |key| key.is_a?(String) ? key.to_sym : key }

Accidentally mutating a hash

transform_keys! changes the original hash. Prefer transform_keys when other code may still need the original string-keyed hash.

Symbolizing untrusted, arbitrary input

Do not blindly turn every user-controlled string into a symbol. Modern Ruby garbage-collects many dynamically created symbols, but creating large numbers of unnecessary symbols is still wasteful. For arbitrary external fields, string keys are often more appropriate.

Using unsafe YAML loading for untrusted files

YAML.load_file can construct objects depending on the YAML content and Ruby/Psych configuration. Do not load YAML supplied by untrusted users. For untrusted or restricted configuration, prefer YAML.safe_load with an appropriate permitted-class list.

Comparisons

ApproachHandles nested hashes?Mutates original?Best use
hash.transform_keys(&:to_sym)NoNoA known, one-level hash
hash.transform_keys!(&:to_sym)NoYesOne-level hash that you own
Recursive symbolize_keys helperYes, including arraysNoPlain Ruby and nested YAML
hash.symbolize_keys (Rails)NoNoOne-level Rails hash
hash.deep_symbolize_keys (Rails)YesNoNested Rails data
Keep string keysNot applicableNot applicableJSON/API data and arbitrary external fields

Symbols are often used for a fixed set of internal option names. Strings are often better for data whose keys come from outside the application or must be preserved exactly.

Take Quiz

Cheat Sheet

# One level; returns a new hash
symbol_hash = string_hash.transform_keys(&:to_sym)

# One level; changes the same hash
string_hash.transform_keys!(&:to_sym)

# Safer when keys may not all be strings
symbol_hash = hash.transform_keys do |key|
  key.is_a?(String) ? key.to_sym : key
end

# Rails: one level
hash.symbolize_keys

# Rails: nested hashes
hash.deep_symbolize_keys

Key rules:

  • "name" and :name are different hash keys.
  • transform_keys changes only one hash level.
  • Recurse through both Hash and Array for nested YAML structures.
  • Prefer one key convention after loading data.
  • Do not use YAML.load_file with YAML from untrusted sources.

FAQ

How do I convert hash keys from strings to symbols in Ruby?

For one level, use hash.transform_keys(&:to_sym). Use a recursive helper when the hash contains nested hashes or arrays.

Does transform_keys(&:to_sym) modify the original hash?

No. It returns a new hash. Use transform_keys! if you intentionally want to modify the original.

Why does my_hash[:key] return nil after loading YAML?

The loaded hash likely has the string key "key". Ruby does not treat "key" and :key as equivalent.

Does transform_keys convert nested YAML keys?

No. It converts only keys in the current hash. Use recursion, or deep_symbolize_keys in Rails.

Is symbolize_keys part of Ruby itself?

No. symbolize_keys and deep_symbolize_keys are Active Support methods supplied by Rails. Core Ruby provides transform_keys.

Should I always use symbol keys in Ruby hashes?

No. Symbols are useful for fixed internal option names. Keep strings for JSON-like data, arbitrary external keys, or keys that must retain their exact original form.

Is YAML safe to load from users?

No. Do not use YAML.load_file on untrusted YAML. Use YAML.safe_load with restrictions appropriate for your data.

Take Quiz

Mini Project

Description

Build a small YAML configuration loader that converts nested string keys into symbols. This mirrors how an application can read a configuration file once and then use predictable Ruby-style access throughout the codebase.

Goal

Load nested YAML configuration data and access every configuration key with symbols.

Requirements

Create a YAML file containing a nested database section and a features list. Load the YAML file in Ruby. Convert string keys at every nesting level, including hashes inside arrays. Print the database host using symbol-key access. Print the first feature name using symbol-key access.

Take Quiz

Keep learning

Related questions

View more