How to load a list of custom objects with SnakeYaml
Asked Answered
P

2

9

I've been trying to deserialize the following yaml to a List<Stage> using SnakeYaml:

- name: Stage1
  items: 
    - item1
    - item2

- name: Stage2
  items: 
    - item3

public class Stage {
    private String name;
    private List<String> items;

    public Stage() {
    }

    public Stage(String name, List<String> items) {
        this.name = name;
        this.items = items;
    }

    // getters and setters
}

The closest question I found was SnakeYaml Deserialise Class containing a List of Objects. After reading it, I am aware of Constructor and TypeDescriptor classes, but I am still unable to get it working (I get list of HashMaps, not Stages).

The difference with the question in the link above is that my top-level structure is a list, not a custom object.

Pewee answered 17/5, 2019 at 13:50 Comment(3)
Did you find a method for this?Daiseydaisi
@ Niveathika No, I gave up and changed the file format.Pewee
I did a hack to solve this, I first used yaml.load() to get array list, then for each element yaml dumped it and used loadAs(). Not the best way there is, but it gets the work done.Daiseydaisi
S
8

One way would be to create your own snakeyaml Constructor like this:

public class ListConstructor<T> extends Constructor {
  private final Class<T> clazz;

  public ListConstructor(final Class<T> clazz) {
    this.clazz = clazz;
  }

  @Override
  protected Object constructObject(final Node node) {
    if (node instanceof SequenceNode && isRootNode(node)) {
      ((SequenceNode) node).setListType(clazz);
    }
    return super.constructObject(node);
  }

  private boolean isRootNode(final Node node) {
    return node.getStartMark().getIndex() == 0;
  }
}

and then use it when constructing the Yaml:

final Yaml yaml = new Yaml(new ListConstructor<>(Stage.class));
Substantial answered 13/11, 2019 at 15:18 Comment(1)
I found isRootNode() returned false always (probably due to my yaml having a comment at the top). Instead I just set a boolean for isRootNode which was initially true then set to false when the SequenceNode was found (also this is less code :) )Unkennel
C
1

If you don't mind bringing in a dependency on Jackson and Jackson Dataformat YAML, this can be achieved in a single line of code, using Jackson's YAMLMapper in conjunction with a TypeReference:

import java.io.InputStream;
import java.util.Arrays;
import java.util.List;
import org.junit.jupiter.api.Test;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.dataformat.yaml.YAMLMapper;
import static org.junit.jupiter.api.Assertions.assertEquals;

public class StageListTest
{
    @Test
    public void testLoadStage() throws Exception
    {
        // The YAML input file:
        InputStream yaml = getClass().getClassLoader().getResourceAsStream("stagelist.yml");

        // The expected result:
        List<Stage> expected = Arrays.asList
        (
            new Stage("Stage1", Arrays.asList("item1", "item2")),
            new Stage("Stage2", Arrays.asList("item3"))
        );

        // Use Jackson YAMLMapper to load file:
        List<Stage> result = new YAMLMapper().readValue(yaml, new TypeReference<List<Stage>>() {});

        // Make sure result is as expected:
        assertEquals(expected, result);
    }
}

(on top of getters and setters, hashCode and equals methods were added to the Stage type to support comparison of Stage objects)

The above solution uses SnakeYAML under the hood, but does not require creation of a custom type constructor.

Caponize answered 14/3, 2023 at 3:41 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.