How to parse part of .yaml file to object in SnakeYAML?
Asked Answered
A

1

6

I have a yaml file, for example:

# this is the part I don't care about
config:
  key-1: val-1
other-config:
  lang: en
  year: 1906
# below is the only part I care about
interesting-setup:
  port: 1234
  validation: false
  parts:
    - on-start: backup
      on-stop: say-goodbye

Also I have a POJO class that is suitable for the interesting-setup part

public class InterestingSetup {
    int port;
    boolean validation;
    List<Map<String, String>> parts;
}

I want to load just the interesting-setup part (similarly as @ConfigurationProperties("interesting-setup") in Spring)

Currently I'm doing it like this:

Map<String, Object> yamlConfig = yaml.load(yamlFile);            # loading the whole file to Map with Object values
Object interestingObject = yamlConfig.get("interesting-setup");  # loading 'interesting-setup' part as an object
Map<String, Object> interestingMap = (Map<String, Object>);      # Casting object to Map<String, Object>
String yamlDumped = yaml.dump(interestingMap);                   # Serialization to String
InterestingSetup finalObject = yaml.load(yamlDumped);            # Getting final object from String

The crucial part is when I have an Object (Map<String, Object>) and want to cast it to my final class. To do that - I need to serialize it to String, so the process looks like this:

File -> Map<String, Object> -> Object -> Map<String, Object> -> String -> FinalClass and I'd like to avoid deserialization and again serialization of the same data.

So can I somehow use Yaml to map the Map<String, Object> to another class? I cannot see this in an API?

Alizaalizarin answered 17/2, 2022 at 17:4 Comment(0)
S
2

AFAIK, the SnakeYAML library doesn't provide a straight way to do that.

You may try tweaking it and defining a container base class with only the fields you required to support. Consider for example the following POJO:

public class Container {
  private InterestingSetup interestingSetup;

  public InterestingSetup getInterestingSetup() {
    return interestingSetup;
  }

  public void setInterestingSetup(InterestingSetup interestingSetup) {
    this.interestingSetup = interestingSetup;
  }

  @Override
  public String toString() {
    return "Container{" +
        "interestingSetup=" + interestingSetup +
        '}';
  }
}

Where, InterestingSetup is your own class:

import java.util.List;
import java.util.Map;

public class InterestingSetup {
  private int port;
  private boolean validation;
  private List<Map<String, String>> parts;

  public int getPort() {
    return port;
  }

  public void setPort(int port) {
    this.port = port;
  }

  public boolean isValidation() {
    return validation;
  }

  public void setValidation(boolean validation) {
    this.validation = validation;
  }

  public List<Map<String, String>> getParts() {
    return parts;
  }

  public void setParts(List<Map<String, String>> parts) {
    this.parts = parts;
  }

  @Override
  public String toString() {
    return "InterestingSetup{" +
        "port=" + port +
        ", validation=" + validation +
        ", parts=" + parts +
        '}';
  }
}

With those beans in place, the following code would work as you required:

import java.io.ByteArrayInputStream;
import java.io.InputStream;
import java.io.UnsupportedEncodingException;
import java.nio.charset.StandardCharsets;

import org.yaml.snakeyaml.TypeDescription;
import org.yaml.snakeyaml.Yaml;
import org.yaml.snakeyaml.constructor.Constructor;
import org.yaml.snakeyaml.representer.Representer;

public class Main {
  public static void main(String... args) throws UnsupportedEncodingException {
    String yamlString =
        "# this is the part I don't care about\n" +
        "config:\n" +
        "  key-1: val-1\n" +
        "other-config:\n" +
        "  lang: en\n" +
        "  year: 1906\n" +
        "# below is the only part I care about\n" +
        "interesting-setup:\n" +
        "  port: 1234\n" +
        "  validation: false\n" +
        "  parts:\n" +
        "    - on-start: backup\n" +
        "      on-stop: say-goodbye";

    // Skip unknown properties
    Representer representer = new Representer();
    representer.getPropertyUtils().setSkipMissingProperties(true);

    // Define the target object type
    Constructor constructor = new Constructor(Container.class);
    TypeDescription containerTypeDescription = new TypeDescription(Container.class);

    // Define how the interesting-setup property should be processed
    containerTypeDescription.substituteProperty("interesting-setup", InterestingSetup.class,
        "getInterestingSetup", "setInterestingSetup");
    constructor.addTypeDescription(containerTypeDescription);

    // Finally, parse the YAML
    Yaml yaml = new Yaml(constructor, representer);
    InputStream inputStream = new ByteArrayInputStream(yamlString.getBytes(StandardCharsets.UTF_8));;
    Container container = yaml.load(inputStream);
    System.out.println(container.getInterestingSetup());
  }
}

Perhaps, a more simple solution will consists on using some method that allows you, given a bunch of fields and their corresponding values, to set the appropriate information in the InterestedSetup bean. You can use the Reflection API for that. The populate method in the BeansUtils class from Apache Commons can also be handy as well:

Map<String, Object> yamlConfig = yaml.load(yamlFile); 
Object interestingObject = yamlConfig.get("interesting-setup");
Map<String, Object> interestingMap = (Map<String, Object>);
InterestingSetup finalObject = BeanUtils.populate(interestingMap);

As an alternate approach, you can use Jackson to process the YAML file. The code will be similar to this:

ObjectMapper mapper = new ObjectMapper(new YAMLFactory());
// As the helper object Container doesn't contain all the properties
// it is necessary to indicate that fact to the library to avoid
// errors
mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
Container container = mapper.readValue(yamlString, Container.class);
System.out.println(container.getInterestingSetup());

The Container class is the same presented above with the addition of a @JsonProperty annotation in order to successfully handle the interesting-setup field:

import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonProperty;

// Instead of mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false)
// you can annotate the class with @JsonIgnoreProperties(ignoreUnknown = true)
// to avoid errors related to unknown properties
public class Container {

  @JsonProperty("interesting-setup")
  private InterestingSetup interestingSetup;

  public InterestingSetup getInterestingSetup() {
    return interestingSetup;
  }

  public void setInterestingSetup(InterestingSetup interestingSetup) {
    this.interestingSetup = interestingSetup;
  }

  @Override
  public String toString() {
    return "Container{" +
        "interestingSetup=" + interestingSetup +
        '}';
  }
}

The required artifacts can be downloaded from Maven as the following dependency:

<dependency>
    <groupId>com.fasterxml.jackson.dataformat</groupId>
    <artifactId>jackson-dataformat-yaml</artifactId>
    <version>2.13.1</version>
</dependency>
Sawyere answered 1/3, 2022 at 20:9 Comment(2)
Wrapping this in the Container class is very neat, thank you, you gave many possible solutions and this is very useful.Alizaalizarin
Sorry for the late reply. Thank you very much @Benjamin. I am very happy to hear that the answer was helpful.Sawyere

© 2022 - 2024 — McMap. All rights reserved.