javasnakeyaml

Parsing dot separated keys to nested YAML objects with SnakeYaml


I have the following HashMap

 Map<String, Object> map = new HashMap<>();
 map.put("a.b", "c");
 map.put("a.d", "e");

And the following code to write the map to a YAML file with SnakeYAML

 DumperOptions options = new DumperOptions();
 options.setDefaultFlowStyle(FlowStyle.BLOCK);
 options.setPrettyFlow(true);
 Yaml yaml = new Yaml(options);
 String path = System.getProperty("user.dir") + "file.yaml";

 try (FileWriter fileWriter = new FileWriter(path)) {
   yaml.dump(map, fileWriter);
 } catch (IOException e) {
   throw new RuntimeException(e);
 }

After cat file.yaml I can see that the file contains the following

a.b: c
a.d: e

However, I'm looking to format the output so that the dot separated keys are in fact nested objects

a:
  b: c
  d: e

I have tried changing options.setIdent() and changing the FlowStyle to FLOW, but so far I've had no luck.

How can I do this in Java with SnakeYAML? Or do I have change my approach and use another method?


Solution

  • I don't think you'll be able to configure SnakeYAML to split the keys because as far as it is concerned, a.b and a.d are valid keys, hence it is generating the YAML with those keys. If you have control over how the input map is formed, then the best thing to do would be to create a map with nested maps, with keys the way you expect in the final yaml. Something like the below:

    Map<String, Object> input = new HashMap<>();
    Map<String, Object> innerMap = new HashMap<>();
    innerMap.put("b", "c");
    innerMap.put("d", "e");
    input.put("a", innerMap);
    

    On the other hand, if you don't have control over how the input map is formed, then you can do some preprocessing, to split the input keys and form nested maps, before passing the map to SnakeYAML. The code might look something like the below:

    public static String toYamlNestedKeys(Map<String, Object> input) {
        prepareInput(input);
    
        DumperOptions dumperOptions = new DumperOptions();
        dumperOptions.setDefaultFlowStyle(FlowStyle.BLOCK);
        Yaml yaml = new Yaml(dumperOptions);
        String result = yaml.dump(input);
    
        LOGGER.info("Result: \n{}", result);
        return result;
    }
    
    private static void prepareInput(Map<String, Object> input) {
        List<String> keysToRemove = new ArrayList<>();
        Map<String, Object> output = new HashMap<>();
    
        input.forEach((key, value) -> {
            if (key.contains(".")) {
                keysToRemove.add(key);
                output.putAll(mergeValues(output, splitKey(key, value)));
            }
        });
    
        // Remove entries that have been converted into nested maps
        keysToRemove.forEach(key -> {
            input.remove(key);
        });
    
        // Add the nested maps
        output.forEach((key, value) -> {
            input.put(key, value);
        });
    }
    
    private static Map<String, Object> mergeValues(Map<String, Object> completeOutput, Map<String, Object> tempOutput) {
        Map.Entry<String, Object> entry = tempOutput.entrySet().stream().findFirst().get();
        Map<String, Object> output = new HashMap<>();
        output.putAll(completeOutput);
    
        if (completeOutput.containsKey(entry.getKey())) {
            output.put(entry.getKey(),
                mergeValues(
                        (Map<String, Object>) completeOutput.get(entry.getKey()),
                        (Map<String, Object>) entry.getValue()
                )
            );
        } else {
            output.put(entry.getKey(), entry.getValue());
        }
    
        return output;
    }
    
    private static Map<String, Object> splitKey(String key, Object value) {
        Map<String, Object> output = new HashMap<>();
    
        int dotIndex = key.lastIndexOf('.');
        if (dotIndex == -1 || dotIndex == key.length() - 1) {
            output.put(key, value);
        } else {
            String innerKey = key.substring(dotIndex + 1, key.length());
            Map<String, Object> innerObject = new HashMap<>();
            innerObject.put(innerKey, value);
    
            String outerKey = key.substring(0, dotIndex);
            output.putAll(splitKey(outerKey, innerObject));
        }
    
        return output;
    }
    

    Below is a unit test, written in Spock, that tests the above code:

    void 'test to yaml nested keys'() {
        given:
        String expectedOutput = '''
        a:
          b: c
          d: e
          f:
            g: h
        i.: j
        k: l
        m:
          n: o
        p:
          q:
            r: s
        '''
    
        and:
        def input = [
                'a.b': 'c',
                'a.d': 'e',
                'a.f.g': 'h',
                'i.': 'j',
                'k': 'l',
                'm': ['n': 'o'],
                'p.q': ['r': 's'],
        ]
    
        when:
        String output = TestUtil.toYamlNestedKeys(input)
    
        then:
        Yaml yaml = new Yaml()
        yaml.load(output) == yaml.load(expectedOutput)
    }