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
:keyhas 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.
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:
inputis aHash, so it creates a new empty result hash.- It sees the key
"database", converts it to:database, and recursively processes its value. - The database value is another
Hash. Its"host"key becomes:host; its string value stays"localhost". - It sees the key
"features", converts it to:features, and recursively processes its value. - The features value is an
Array, so Ruby maps over every item in it. - The array item is a hash, so its
"name"key becomes:name. - 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.
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
| Approach | Handles nested hashes? | Mutates original? | Best use |
|---|---|---|---|
hash.transform_keys(&:to_sym) | No | No | A known, one-level hash |
hash.transform_keys!(&:to_sym) | No | Yes | One-level hash that you own |
Recursive symbolize_keys helper | Yes, including arrays | No | Plain Ruby and nested YAML |
hash.symbolize_keys (Rails) | No | No | One-level Rails hash |
hash.deep_symbolize_keys (Rails) | Yes | No | Nested Rails data |
| Keep string keys | Not applicable | Not applicable | JSON/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.
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:nameare different hash keys.transform_keyschanges only one hash level.- Recurse through both
HashandArrayfor nested YAML structures. - Prefer one key convention after loading data.
- Do not use
YAML.load_filewith 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.
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.
Keep learning
Related questions
Add a Default Value to an Existing Column in a Rails Migration
Learn how to add or change a default value on an existing database column using Rails Active Record migrations safely.
Calling a Class Method from an Instance in Ruby
Learn how to call a class method from an instance in Ruby using self.class, with examples, pitfalls, and practical usage patterns.
Calling an Overridden Monkey-Patched Method in Ruby
Learn how to call the original method when monkey patching in Ruby, including alias_method patterns, examples, pitfalls, and practical usage.