Search code examples
jsonrubyjsonparser

Find line number of key in JSON


Given a JSON file and the path to a key, I need to be able to get the line number where the value is stored. Values that need multiple lines (for example, arrays) are currently out of the scope until I sort out how to deal with the simplest case. For example, in the following JSON:

{                        # Line 1
    "foo": {             # Line 2
        "bar": [
            "value1",
            "value2"
        ],
        "bar2": 2        # Line 7
    },
    "bar": {
        "bar": [
            "value1",
            "value2"
        ],
        "bar2": 5
    }
}

I should get 7 when looking for the key path foo.bar2.

I have a working solution assuming that the original file is formatted with JSON.pretty_generate:

parsed_json = File.read(file)
random_string = SecureRandom.uuid

parsed_json.bury(*path.split('.'), random_string)

JSON.pretty_generate(parsed_json)
    .split("\n")
    .take_while { |s| s.exclude? random_string }
    .count + 1

What I am doing here is parsing the JSON file, replacing the existing value with a random string (a UUID in this case), formatting the hash into pretty-printed JSON, and finding the line where the random string is present. The Hash.bury method used here works as defined in https://bugs.ruby-lang.org/issues/11747.

This solution is working fine (not heavily tested yet, though), but I am struggling to make it work when the original file is not formatted as a pretty-printed JSON. For example, the following file would be equivalent to the one above:

{                                     # Line 1
    "foo": {                          # Line 2
        "bar": ["value1","value2"],  
        "bar2": 2                     # Line 4
    },
    "bar": {
        "bar": [
            "value1",
            "value2"
        ],
        "bar2": 5
    }
}

but the algorithm above would still return 7 as the line where foo.bar2 is located, although now it is in line 4.

Is there any way to reliably get the line number where a key is placed inside the JSON file?


Solution

  • Here is the easiest way I found without building your own JSON parser: replace every key entry with unique UUID (alias), then build all combinations of aliases and find that one that returns data from #dig call

    keys = path.split('.')
    file_content = File.read(file_path).gsub('null', '1111')
    aliases = {}
    
    keys.each do |key|
      pattern = "\"#{key}\":"
    
      file_content.scan(pattern).each do
        alias_key = SecureRandom.uuid
        file_content.sub!(pattern, "\"#{alias_key}\":")
    
        aliases[key] ||= []
        aliases[key] << alias_key
      end
    end
    
    winner = aliases.values.flatten.combination(keys.size).find do |alias_keys|
      # nulls were gsubbed above to make this check work in edge case when path value is null
      JSON.parse(file_content).dig(*alias_keys).present?
    end
    
    file_content.split("\n").take_while { |line| line.exclude?(winner.last) }.count + 1
    

    UPD: The snippet above should not work if JSON value by your foo.bar2 keys is false. You should gsub it as well or make this snippet smarter