Parsing a YAML document with a map at the root using snakeYaml
Asked Answered
T

5

12

I want to read a YAML document to a map of custom objects (instead of maps, which snakeYaml does by default). So this:

19:
  typeID: 2
  limit: 300
20:
  typeID: 8
  limit: 100

Would be loaded to a map which looks like this:

Map<Integer, Item>

where Item is:

class Item {
    private Integer typeId;
    private Integer limit;
}

I could not find a way to do this with snakeYaml, and I couldn't find a better library for the task either.

The documentation only has examples with maps/collections nested inside other objects, so that you can do the following:

    TypeDescription typeDescription = new TypeDescription(ClassContainingAMap.class);
    typeDescription.putMapPropertyType("propertyNameOfNestedMap", Integer.class, Item.class);
    Constructor constructor = new Constructor(typeDescription);
    Yaml yaml = new Yaml(constructor);
    /* creating an input stream (is) */
    ClassContainingAMap obj = (ClassContainingAMap) yaml.load(is);

But how do I go about defining the Map format when it is at the root of the document?

Tjaden answered 16/2, 2015 at 21:59 Comment(0)
S
5

You need to add a custom Constructor. However, in your case you don't want register an "item" or "item-list" tag.

In effect, you want to apply Duck Typing to your Yaml. It's not super efficient, but there is a relatively easy way to do this.

class YamlConstructor extends Constructor {
  @Override
  protected Object constructObject(Node node) {

    if (node.getTag() == Tag.MAP) {
        LinkedHashMap<String, Object> map = (LinkedHashMap<String, Object>) super
                .constructObject(node);
        // If the map has the typeId and limit attributes
        // return a new Item object using the values from the map
        ...
    }
     // In all other cases, use the default constructObject.
    return super.constructObject(node);
Swage answered 16/2, 2015 at 23:15 Comment(6)
Wow, YAML support in Java is apalling. While this solution works, it would be a headache when having to deal with nested structures. I guess I'll just convert everything to JSON and use Jackson for parsing.Tjaden
Yes. Yaml parsing in java (/scala) is terrible. Coming from python where it is "free".Seventieth
@javadba absolutely. Having come from python, half the time I find myself fighting the language to get out of my way. Feel very constrained and lacking absolutely any freedom at all.Despairing
There has to be a better way than this! (I'd give a better answer... if only I knew how :-)Calpe
@Tjaden why don't just directly use Jackson to read the YAML? For example: baeldung.com/jackson-yamlAdlei
@Adlei if you suggest using another library, please provide the source code on how to do what OP is asking for. ThanksGoerke
E
13

Here is what I did for a very similar situation. I just tabbed my whole yml file over one tab and added a map: tag to the top. So for your case it would be.

map:
  19:
    typeID: 2
    limit: 300
  20:
    typeID: 8
    limit: 100

And then create a static class in your class that reads this file like follows.

static class Items {
    public Map<Integer, Item> map;
}

And then to read your map just use.

Yaml yaml = new Yaml(new Constructor(Items));
Items items = (Items) yaml.load(<file>);
Map<Integer, Item> itemMap = items.map;

UPDATE:

If you don't want to or cannot edit your yml file you could just do the above transform in code while reading the file with something like this.

try (BufferedReader br = new BufferedReader(new FileReader(new File("example.yml")))) {
    StringBuilder builder = new StringBuilder("map:\n");
    String line;
    while ((line = br.readLine()) != null) {
        builder.append("    ").append(line).append("\n");
    }

    Yaml yaml = new Yaml(new Constructor(Items));
    Items items = (Items) yaml.load(builder.toString());
    Map<Integer, Item> itemMap = items.map;
}
Engram answered 2/3, 2017 at 20:36 Comment(3)
This is actually really clever if you're OK with modifying or creating your own files.Tjaden
See update above for how to pull the same trick without having to modify the underlying file.Engram
This is pretty much a hack. But it beats out anything else I've seen for parsing yaml by java (scala actually in my case !). Upvoted.Seventieth
S
5

You need to add a custom Constructor. However, in your case you don't want register an "item" or "item-list" tag.

In effect, you want to apply Duck Typing to your Yaml. It's not super efficient, but there is a relatively easy way to do this.

class YamlConstructor extends Constructor {
  @Override
  protected Object constructObject(Node node) {

    if (node.getTag() == Tag.MAP) {
        LinkedHashMap<String, Object> map = (LinkedHashMap<String, Object>) super
                .constructObject(node);
        // If the map has the typeId and limit attributes
        // return a new Item object using the values from the map
        ...
    }
     // In all other cases, use the default constructObject.
    return super.constructObject(node);
Swage answered 16/2, 2015 at 23:15 Comment(6)
Wow, YAML support in Java is apalling. While this solution works, it would be a headache when having to deal with nested structures. I guess I'll just convert everything to JSON and use Jackson for parsing.Tjaden
Yes. Yaml parsing in java (/scala) is terrible. Coming from python where it is "free".Seventieth
@javadba absolutely. Having come from python, half the time I find myself fighting the language to get out of my way. Feel very constrained and lacking absolutely any freedom at all.Despairing
There has to be a better way than this! (I'd give a better answer... if only I knew how :-)Calpe
@Tjaden why don't just directly use Jackson to read the YAML? For example: baeldung.com/jackson-yamlAdlei
@Adlei if you suggest using another library, please provide the source code on how to do what OP is asking for. ThanksGoerke
C
3

To keep type safety, you need to take control of the root node construction. To do this, you can use the rootTag property of the Constructor to mark the root node, fix the type of the children, and construct the map.

The custom constructor should look like the following

public static class MyConstructor extends Constructor {
    private TypeDescription itemType = new TypeDescription(Item.class);

    public MyConstructor() {
        this.rootTag = new Tag("myRoot");
        this.addTypeDescription(itemType);
    }

    @Override
    protected Object constructObject(Node node) {
        if ("myRoot".equals(node.getTag().getValue()) && node instanceof MappingNode) {
            MappingNode mNode = (MappingNode) node;
            return mNode.getValue().stream().collect(
                    Collectors.toMap(
                            t -> super.constructObject(t.getKeyNode()),
                            t -> {
                                Node child = t.getValueNode();
                                child.setType(itemType.getType());
                                return super.constructObject(child);
                            }
                    )
            );

        } else {
            return super.constructObject(node);
        }
    }
}

Here's fully functional example

import org.yaml.snakeyaml.TypeDescription;
import org.yaml.snakeyaml.Yaml;
import org.yaml.snakeyaml.constructor.Constructor;
import org.yaml.snakeyaml.nodes.MappingNode;
import org.yaml.snakeyaml.nodes.Node;
import org.yaml.snakeyaml.nodes.Tag;

import java.util.Map;
import java.util.stream.Collectors;

public class Foo {

public static void main(String[] args) {
    String theYaml = "19:\n" +
            "  typeID: 2\n" +
            "  limit: 300\n" +
            "20:\n" +
            "  typeID: 8\n" +
            "  limit: 100";

    Yaml yaml = new Yaml(new MyConstructor());
    Map<String, Item> data = yaml.load(theYaml);
    System.out.println(data);
}

public static class Item {
    private int typeID, limit;

    public int getTypeID() {
        return typeID;
    }

    public void setTypeID(int typeID) {
        this.typeID = typeID;
    }

    public int getLimit() {
        return limit;
    }

    public void setLimit(int limit) {
        this.limit = limit;
    }
}

public static class MyConstructor extends Constructor {
    private TypeDescription itemType = new TypeDescription(Item.class);

    public MyConstructor() {
        this.rootTag = new Tag("myRoot");
        this.addTypeDescription(itemType);
    }

    @Override
    protected Object constructObject(Node node) {
        if ("myRoot".equals(node.getTag().getValue()) && node instanceof MappingNode) {
            MappingNode mNode = (MappingNode) node;
            return mNode.getValue().stream().collect(
                    Collectors.toMap(
                            t -> super.constructObject(t.getKeyNode()),
                            t -> {
                                Node child = t.getValueNode();
                                child.setType(itemType.getType());
                                return super.constructObject(child);
                            }
                    )
            );

        } else {
            return super.constructObject(node);
        }
    }
}
}

If you don't care about the types, and just want duck typing (everything is a map), You can load with no settings.

Java

Map<String, Object> data = new Yaml().load(yamldata);

Scala

val data: java.util.Map[String, Any] = new Yaml().load(yamlData)
Conlen answered 7/11, 2018 at 22:10 Comment(2)
I think you can definitely do this. But objects in the map will not be constructucte into objects of type Item. So you are left to deal with maps all the way instead properly 'type safe' objects of type 'Item' with proper accessor methods. So I don't think this is an answer to the question.Calpe
@Calpe You're totally right, I didn't catch that the OP was trying to keep the type of the children. I've updated the answer to reflect the proper solution.Conlen
M
0

All credit goes to the answer by Snowbldr. This answer is an upgrade for SnakeYaml 2.x, in which the constructor of Contructor now takes an instance of LoaderOptions. In addition, the class of the values in the Map can be passed as an argument.

public static class TypedMapConstructor<T> extends Constructor {
    private final TypeDescription itemType;

    public TypedMapConstructor(Class<T> itemClass, LoaderOptions loaderOptions) {
        super(loaderOptions);
        this.itemType = new TypeDescription(itemClass);
        this.rootTag = new Tag("ROOT");
        this.addTypeDescription(itemType);
    }

    @Override
    protected Object constructObject(Node node) {
        if (!isRootNode(node)) {
            return super.constructObject(node);
        }

        return ((MappingNode) node).getValue().stream().collect(
                Collectors.toMap(
                        nodeTuple -> super.constructObject(nodeTuple.getKeyNode()),
                        nodeTuple -> {
                            Node valueNode = nodeTuple.getValueNode();
                            valueNode.setType(itemType.getType());
                            return super.constructObject(valueNode);
                        }
                )
        );
    }

    private boolean isRootNode(Node node) {
        return node.getTag().getValue().equals(rootTag.getValue());
    }
}
Yaml yaml = new Yaml(new TypedMapConstructor<>(Item.class, new LoaderOptions()));
Millionaire answered 13/8, 2023 at 16:5 Comment(0)
I
-1

As of Groovy 3, you can use Groovy to parse YAML.

import groovy.yaml.YamlSlurper

...

def yaml = new YamlSlurper().parseText(text)

If you want to map it into your custom type, you could do something like this.

yaml.collect({key, item -> new Item(typeId: item?.typeID, limit: item?.limit)})

I realize this kind of support for YAML in Groovy has only been available since 2019. But now that Groovy does provide a simple option for JVM-based systems (i.e. it can be mixed with Java), I figured it was worth posting for others who might be having similar struggles.

Iddo answered 17/8, 2021 at 20:13 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.