How do I convert a String object into a Hash object?

Viewed 232447

I have a string which looks like a hash:

"{ :key_a => { :key_1a => 'value_1a', :key_2a => 'value_2a' }, :key_b => { :key_1b => 'value_1b' } }"

How do I get a Hash out of it? like:

{ :key_a => { :key_1a => 'value_1a', :key_2a => 'value_2a' }, :key_b => { :key_1b => 'value_1b' } }

The string can have any depth of nesting. It has all the properties how a valid Hash is typed in Ruby.

16 Answers

This short little snippet will do it, but I can't see it working with a nested hash. I think it's pretty cute though

STRING.gsub(/[{}:]/,'').split(', ').map{|h| h1,h2 = h.split('=>'); {h1 => h2}}.reduce(:merge)

Steps 1. I eliminate the '{','}' and the ':' 2. I split upon the string wherever it finds a ',' 3. I split each of the substrings that were created with the split, whenever it finds a '=>'. Then, I create a hash with the two sides of the hash I just split apart. 4. I am left with an array of hashes which I then merge together.

EXAMPLE INPUT: "{:user_id=>11, :blog_id=>2, :comment_id=>1}" RESULT OUTPUT: {"user_id"=>"11", "blog_id"=>"2", "comment_id"=>"1"}

Here is a method using whitequark/parser which is safer than both gsub and eval methods.

It makes the following assumptions about the data:

  1. Hash keys are assumed to be a string, symbol, or integer.
  2. Hash values are assumed to be a string, symbol, integer, boolean, nil, array, or a hash.
# frozen_string_literal: true

require 'parser/current'

class HashParser
  # Type error is used to handle unexpected types when parsing stringified hashes.
  class TypeError < ::StandardError
    attr_reader :message, :type

    def initialize(message, type)
      @message = message
      @type = type
    end
  end

  def hash_from_s(str_hash)
    ast = Parser::CurrentRuby.parse(str_hash)

    unless ast.type == :hash
      puts "expected data to be a hash but got #{ast.type}"
      return
    end

    parse_hash(ast)
  rescue Parser::SyntaxError => e
    puts "error parsing hash: #{e.message}"
  rescue TypeError => e
    puts "unexpected type (#{e.type}) encountered while parsing: #{e.message}"
  end

  private

  def parse_hash(hash)
    out = {}
    hash.children.each do |node|
      unless node.type == :pair
        raise TypeError.new("expected child of hash to be a `pair`", node.type)
      end

      key, value = node.children

      key = parse_key(key)
      value = parse_value(value)

      out[key] = value
    end

    out
  end

  def parse_key(key)
    case key.type
    when :sym, :str, :int
      key.children.first
    else
      raise TypeError.new("expected key to be either symbol, string, or integer", key.type)
    end
  end

  def parse_value(value)
    case value.type
    when :sym, :str, :int
      value.children.first
    when :true
      true
    when :false
      false
    when :nil
      nil
    when :array
      value.children.map { |c| parse_value(c) }
    when :hash
      parse_hash(value)
    else
      raise TypeError.new("value of a pair was an unexpected type", value.type)
    end
  end
end

and here are some rspec tests verifying that it works as expected:

# frozen_string_literal: true

require 'spec_helper'

RSpec.describe HashParser do
  describe '#hash_from_s' do
    subject { described_class.new.hash_from_s(input) }

    context 'when input contains forbidden types' do
      where(:input) do
        [
          'def foo; "bar"; end',
          '`cat somefile`',
          'exec("cat /etc/passwd")',
          '{:key=>Env.fetch("SOME_VAR")}',
          '{:key=>{:another_key=>Env.fetch("SOME_VAR")}}',
          '{"key"=>"value: #{send}"}'
        ]
      end

      with_them do
        it 'returns nil' do
          expect(subject).to be_nil
        end
      end
    end

    context 'when input cannot be parsed' do
      let(:input) { "{" }

      it 'returns nil' do
        expect(subject).to be_nil
      end
    end

    context 'with valid input' do
      using RSpec::Parameterized::TableSyntax

      where(:input, :expected) do
        '{}'                          | {}
        '{"bool"=>true}'              | { 'bool' => true }
        '{"bool"=>false}'             | { 'bool' => false }
        '{"nil"=>nil}'                | { 'nil' => nil }
        '{"array"=>[1, "foo", nil]}'  | { 'array' => [1, "foo", nil] }
        '{foo: :bar}'                 | { foo: :bar }
        '{foo: {bar: "bin"}}'         | { foo: { bar: "bin" } }
      end

      with_them do
        specify { expect(subject).to eq(expected) }
      end
    end
  end
end

This method works for one level deep hash


  def convert_to_hash(str)
    return unless str.is_a?(String)

    hash_arg = str.gsub(/[^'"\w\d]/, ' ').squish.split.map { |x| x.gsub(/['"]/, '') }
    Hash[*hash_arg]
  end

example


> convert_to_hash("{ :key_a => 'value_a', :key_b => 'value_b', :key_c => '' }")
=> {"key_a"=>"value_a", "key_b"=>"value_b", "key_c"=>""}


Ran across a similar issue that needed to use the eval().

My situation, I was pulling some data from an API and writing it to a file locally. Then being able to pull the data from the file and use the Hash.

I used IO.read() to read the contents of the file into a variable. In this case IO.read() creates it as a String.

Then used eval() to convert the string into a Hash.

read_handler = IO.read("Path/To/File.json")

puts read_handler.kind_of?(String) # Returns TRUE

a = eval(read_handler)

puts a.kind_of?(Hash) # Returns TRUE

puts a["Enter Hash Here"] # Returns Key => Values

puts a["Enter Hash Here"].length # Returns number of key value pairs

puts a["Enter Hash Here"]["Enter Key Here"] # Returns associated value

Also just to mention that IO is an ancestor of File. So you can also use File.read instead if you wanted.

I had a similar issue when trying to convert a string to a hash in Ruby.

The result from my computations was this:

{
 "coord":{"lon":24.7535,"lat":59.437},
 "weather":[{"id":803,"main":"Clouds","description":"broken clouds","icon":"04d"}],
 "base":"stations",
 "main":{"temp":283.34,"feels_like":281.8,"temp_min":282.33,"temp_max":283.34,"pressure":1021,"humidity":53},
 "visibility":10000,
 "wind":{"speed":3.09,"deg":310},
 "clouds":{"all":75},
 "dt":1652808506,
 "sys":{"type":1,"id":1330,"country":"EE","sunrise":1652751796,"sunset":1652813502},
 "timezone":10800,"id":588409,"name":"Tallinn","cod":200
 }

I checked the type value and confirmed that it was of the String type using the command below:

result = 
{
 "coord":{"lon":24.7535,"lat":59.437},
 "weather":[{"id":803,"main":"Clouds","description":"broken clouds","icon":"04d"}],
 "base":"stations",
 "main":{"temp":283.34,"feels_like":281.8,"temp_min":282.33,"temp_max":283.34,"pressure":1021,"humidity":53},
 "visibility":10000,
 "wind":{"speed":3.09,"deg":310},
 "clouds":{"all":75},
 "dt":1652808506,
 "sys":{"type":1,"id":1330,"country":"EE","sunrise":1652751796,"sunset":1652813502},
 "timezone":10800,"id":588409,"name":"Tallinn","cod":200
 }

puts result.instance_of? String
puts result.instance_of? Hash

Here's how I solved it:

All I had to do was run the command below to convert it from a String to a Hash:

result_new = JSON.parse(result, symbolize_names: true)

And then checked the type value again using the commands below:

puts result_new.instance_of? String
puts result_new.instance_of? Hash

This time it returned true for the Hash

Related