Support bean properties of type java.util.Optional<T>

Issue #310 resolved
Ricardo Rocha created an issue

Bean properties of type java.util.Optional<T> are currently not recognized by SnakeYAML. Please add java.util.Optional to the list of natively supported Java types.

Given the following class:

class Person {
    private int id;
    private String name;
    private java.util.Optional<Double> income;
    // accessors elided ...
}

and the following YAML content:

id: 123
name: Neo Anderson
income: 123456.78

SnakeYAML will fail to load with:

Caused by: org.yaml.snakeyaml.error.YAMLException: No single argument constructor found for class java.util.Optional
    at org.yaml.snakeyaml.constructor.Constructor$ConstructScalar.construct(Constructor.java:391)
    at org.yaml.snakeyaml.constructor.BaseConstructor.constructObject(BaseConstructor.java:182)
    at org.yaml.snakeyaml.constructor.Constructor$ConstructMapping.constructJavaBean2ndStep(Constructor.java:297)
    ... 8 more

The expected behavior is for a present mapping value to be wrapped (by means of Optional.of()) as a non-empty optional value. Correspondingly, empty mapping values would yield Optional.empty().

Comments (16)

  1. Andrey Somov

    java.util.Optional is Java 8, SnakeYAML is now at Java 6 with no plans to migrate to Java 7. Feel free to implement it it in your custom code under Java 8 and share with us your implementation, then we can put it to the "how to" wiki page and spread the knowledge.

  2. Alexander Maslov

    Andrey is right, but it reveals another issue which I consider a BUG. I would expect this example to work because there is Option(T) constructor, but it is private. When we construct JavaBean we ignore access level of its constructor, but not in this case (only public constructors checked). @asomov , do you think it deserves its own issue or it is not an issue at all?

  3. Alexander Maslov

    but even if we fix issue with "private constructors for scalars" I am not sure this issue will be fixed because of the Generic nature of the Optional.

  4. Andrey Somov

    I think we may try to look at it as "single argument scalar". But we cannot fix it because we use Java 6. It has to be fixed in a branch for Java 8.

  5. Alexander Maslov

    just pushed some changes, you can try if it helps with Optional (it may help, but it is NOT real full support of Optional type).

    Andrey, check if you are fine with these changes (especially ConstructScalar.constructStandardJavaInstance), because the part "Standard" in the name now a bit misleading.

  6. Former user Account Deleted

    Concrete test case:

    import org.junit.Test;
    import org.yaml.snakeyaml.Yaml;
    
    import java.util.Optional;
    
    public class SnakeYAMLTest {
    
        public static class Salary {
    
            private Optional<Double> income = Optional.empty();
    
            public Optional<Double> getIncome() {
                return income;
            }
    
            public void setIncome(Double income) {
                this.income = Optional.of(income);
            }
    
            public void setIncome(Optional<Double> income) {
                this.income = income;
            }
    
            @Override
            public String toString() {
                return "Salary{" +
                        "income=" + income +
                        '}';
            }
        }
    
        @Test
        public void testOptionalsInYAM() throws Exception {
            final String yaml = "income: 123456.78";
            final Yaml yamlParser = new Yaml();
            final Salary salary = yamlParser.loadAs(yaml, Salary.class);
            System.out.println(salary);
        }
    }
    

    gives

    Salary{income=Optional[123456.78]}
    
  7. Former user Account Deleted

    However, if the optional contains another bean, there are problems:

    import org.junit.Test;
    import org.yaml.snakeyaml.Yaml;
    
    import java.util.Optional;
    
    public class SnakeYAMLTest {
    
        public static class Salary {
    
            private Optional<Double> income = Optional.empty();
    
            public Optional<Double> getIncome() {
                return income;
            }
    
            public void setIncome(Double income) {
                this.income = Optional.of(income);
            }
    
            public void setIncome(Optional<Double> income) {
                this.income = income;
            }
    
            @Override
            public String toString() {
                return "Salary{" +
                        "income=" + income +
                        '}';
            }
        }
    
        public static class Person {
    
            private String name;
    
            private Optional<Salary> salary = Optional.empty();
    
            public String getName() {
                return name;
            }
    
            public void setName(String name) {
                this.name = name;
            }
    
            public Optional<Salary> getSalary() {
                return salary;
            }
    
            public void setSalary(Optional<Salary> salary) {
                this.salary = salary;
            }
    
            @Override
            public String toString() {
                return "Person{" +
                        "name='" + name + '\'' +
                        ", salary=" + salary +
                        '}';
            }
        }
    
        @Test
        public void testOptionalsInYAM() throws Exception {
            final String yaml = "" +
                    "name: Neo Anderson\n" +
                    "salary:\n" +
                    "   income: 123456.78\n";
            final Yaml yamlParser = new Yaml();
            final Person person = yamlParser.loadAs(yaml, Person.class);
            System.out.println(person);
        }
    }
    

    gives

    Cannot create property=salary for JavaBean=Person{name='Neo Anderson', salary=Optional.empty}
     in 'string', line 1, column 1:
        name: Neo Anderson
        ^
    Cannot create property=income for JavaBean=Optional.empty
     in 'string', line 3, column 4:
           income: 123456.78
           ^
    Unable to find property 'income' on class: java.util.Optional
     in 'string', line 3, column 12:
           income: 123456.78
                   ^
    
     in 'string', line 3, column 4:
           income: 123456.78
           ^
    
        at org.yaml.snakeyaml.constructor.Constructor$ConstructMapping.constructJavaBean2ndStep(Constructor.java:312)
        at org.yaml.snakeyaml.constructor.Constructor$ConstructMapping.construct(Constructor.java:189)
        at org.yaml.snakeyaml.constructor.Constructor$ConstructYamlObject.construct(Constructor.java:345)
        at org.yaml.snakeyaml.constructor.BaseConstructor.constructObject(BaseConstructor.java:182)
        at org.yaml.snakeyaml.constructor.BaseConstructor.constructDocument(BaseConstructor.java:141)
        at org.yaml.snakeyaml.constructor.BaseConstructor.getSingleData(BaseConstructor.java:127)
        at org.yaml.snakeyaml.Yaml.loadFromReader(Yaml.java:450)
        at org.yaml.snakeyaml.Yaml.loadAs(Yaml.java:427)
        at SnakeYAMLTest.testOptionalsInYAM(SnakeYAMLTest.java:70)
        at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
        at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
        at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
        at org.junit.runners.model.FrameworkMethod$1.runReflectiveCall(FrameworkMethod.java:47)
        at org.junit.internal.runners.model.ReflectiveCallable.run(ReflectiveCallable.java:12)
        at org.junit.runners.model.FrameworkMethod.invokeExplosively(FrameworkMethod.java:44)
        at org.junit.internal.runners.statements.InvokeMethod.evaluate(InvokeMethod.java:17)
        at org.junit.runners.ParentRunner.runLeaf(ParentRunner.java:271)
        at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:70)
        at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:50)
        at org.junit.runners.ParentRunner$3.run(ParentRunner.java:238)
        at org.junit.runners.ParentRunner$1.schedule(ParentRunner.java:63)
        at org.junit.runners.ParentRunner.runChildren(ParentRunner.java:236)
        at org.junit.runners.ParentRunner.access$000(ParentRunner.java:53)
        at org.junit.runners.ParentRunner$2.evaluate(ParentRunner.java:229)
        at org.junit.runners.ParentRunner.run(ParentRunner.java:309)
        at org.junit.runner.JUnitCore.run(JUnitCore.java:160)
        at com.intellij.junit4.JUnit4IdeaTestRunner.startRunnerWithArgs(JUnit4IdeaTestRunner.java:78)
        at com.intellij.rt.execution.junit.JUnitStarter.prepareStreamsAndStart(JUnitStarter.java:212)
        at com.intellij.rt.execution.junit.JUnitStarter.main(JUnitStarter.java:68)
        at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
        at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
        at com.intellij.rt.execution.application.AppMain.main(AppMain.java:140)
    Caused by: Cannot create property=income for JavaBean=Optional.empty
     in 'string', line 3, column 4:
           income: 123456.78
           ^
    Unable to find property 'income' on class: java.util.Optional
     in 'string', line 3, column 12:
           income: 123456.78
                   ^
    
        at org.yaml.snakeyaml.constructor.Constructor$ConstructMapping.constructJavaBean2ndStep(Constructor.java:312)
        at org.yaml.snakeyaml.constructor.Constructor$ConstructMapping.construct(Constructor.java:189)
        at org.yaml.snakeyaml.constructor.BaseConstructor.constructObject(BaseConstructor.java:182)
        at org.yaml.snakeyaml.constructor.Constructor$ConstructMapping.constructJavaBean2ndStep(Constructor.java:297)
        ... 34 more
    Caused by: org.yaml.snakeyaml.error.YAMLException: Unable to find property 'income' on class: java.util.Optional
        at org.yaml.snakeyaml.introspector.PropertyUtils.getProperty(PropertyUtils.java:132)
        at org.yaml.snakeyaml.introspector.PropertyUtils.getProperty(PropertyUtils.java:121)
        at org.yaml.snakeyaml.constructor.Constructor$ConstructMapping.getProperty(Constructor.java:322)
        at org.yaml.snakeyaml.constructor.Constructor$ConstructMapping.constructJavaBean2ndStep(Constructor.java:240)
        ... 37 more
    
  8. Alexander Maslov

    @butlermh, just pushed another change about private constructors. It may help a bit, but (as I said before) this is not 1st class support for Optional.

    With those changes loadable yaml for your example would be:

    name: Neo Anderson
    salary: [{income: [123456.78]}]
    

    Unfortunately I cannot give you Representer which will generate the same yaml.

  9. Alexander Maslov

    Few other changes and it seems like possible to dump Optional in a way, that you can load them back.

    public static class OptionalRepresenter extends Representer {
            public OptionalRepresenter() {
                this.representers.put(Optional.class, new RepresentOptional());
            }
    
            private class RepresentOptional implements Represent {
    
                public Node representData(Object data) {
                    Optional<?> opt = (Optional<?>) data;
                    List<Object> seq = new ArrayList<>(1);
                    seq.add(opt.get());
                    return representSequence(Tag.SEQ, seq, true);
                }
            }
        }
    

    plus you need to supply Representer to YAML

    new Yaml(new OptionalRepresenter());
    

    In case of not Scalar inside Optional, dump will contain explicit tags like:

    !!org.yaml.snakeyaml.issues.issue310.OptionalTest$Person
    name: Neo Anderson
    salary: [!!org.yaml.snakeyaml.issues.issue310.OptionalTest$Salary {income: [123456.78]}]
    

    Not sure how well it would work in case of complex Object graph. Try if you have one and don't forget to tell us about your results.

  10. Alexander Maslov

    @asomov , didn't want to lose any effort put into this issue... I have created profile 'with-java8-tests' which adds src/test/java8 as test folder and sets source and target to 1.8 (of course). So now we can keep java8 specific tests in src/test/java8 (before we eventually move to java8) and try them with

    $ mvn -Pwith-java8-tests clean test
    
  11. Andrey Somov

    As far as I understand it can only be released when SnakeYAML switches to Java 8. It will not happen in the short term. (Unless the community insists to break support for Java before version 8)

    You can grab the source, build it and push it to your own Nexus (with a classifier to avoid confusion)

  12. Former user Account Deleted

    OK

    Thanks for all the efforts so far! Specifically for being the only Java implementation with "<<:" aka merge. And for being what Jackson relies upon when handling YAML.

  13. Alexander Maslov

    Alain, this "feature" will be part of the next release (a guess somewhere in February) As I mentioned before - nothing specific for Java8 Optional, but it seems like possible to use them (see those java 8 tests). ATM our code base contains nothing specific to Java 8 (except couple of TESTS for Optional).

    So, if current SNAPSHOT works for you, next release should also.

  14. Log in to comment