allow subclasses of Constructor to provide "soft" override of root document tag

Issue #320 resolved
Jason Sachs created an issue

Constructor doesn't seem to allow you to specify a default root tag without overriding any explicit tags given in the YAML file. (I'm using SnakeYAML 1.15, but 1.16 seems to have the same problem)

I have a YAML document that I want to map to a custom Bean class (TemplateConfiguration). I've been doing this in the past as follows:

 static TemplateConfiguration loadYaml(FileInputStream fileInputStream) {
        Constructor constructor = new Constructor(TemplateConfiguration.class);
        TypeDescription typedesc = new TypeDescription(TemplateConfiguration.class);
        constructor.addTypeDescription(typedesc);
        Yaml yaml = new Yaml(constructor);
        return (TemplateConfiguration)yaml.load(fileInputStream);
    }

Works great.

Recently I have a compatibility issue where I have to change the YAML schema and allow a different one instead. I'm going to use tags so the document can specify !v1 or !v2 or something to specify whether I used TemplateConfigurationV1 or TemplateConfigurationV2 implementation classes in Java (both descendants of a TemplateConfigurationFile interface), with this code to load:

FileInputStream fis = new FileInputStream(file);
Constructor constructor = new Constructor(); 
addTagTypeDescription(constructor, TemplateConfigurationV1.class, "!v1");
addTagTypeDescription(constructor, TemplateConfigurationV2.class, "!v2");
Yaml yaml = new Yaml(constructor);
TemplateConfigurationFile tcfg = (TemplateConfigurationFile)yaml.load(fis);

This also works great. But I need to support old YAML files which don't have a type tag. And I can't get my code to work; BaseConstructor.getSingleData() seems to be the culprit:

    /**
     * Ensure that the stream contains a single document and construct it
     * 
     * @return constructed instance
     * @throws ComposerException
     *             in case there are more documents in the stream
     */
    public Object getSingleData(Class<?> type) {
        // Ensure that the stream contains a single document and construct it
        Node node = composer.getSingleNode();
        if (node != null) {
            if (Object.class != type) {
                node.setTag(new Tag(type));
            } else if (rootTag != null) {
                node.setTag(rootTag);
            }
            return constructDocument(node);
        }
        return null;
    }

Here the node tag override is controlled completely by the type given to getSingleData() and the rootTag; SnakeYAML ignores the node's tag from the YAML document itself.

I figured, great, I'll just subclass Constructor and override getSingleData() to setTag() only if the node's tag matches Tag.MAP. But I can't, because composer and constructObject are private, so my overriden method can't use them. :-(

Please help support this use case, at least by making getSingleData() overrideable in practice.

Comments (4)

  1. Jason Sachs reporter

    could you make a final protected getComposer(), and make final protected constructObject() instead of private?

  2. Jason Sachs reporter

    I found a hacky workaround for the moment.

        /**
         * override of what's in org.yaml.snakeyaml.Yaml to get around issue
         * https://bitbucket.org/asomov/snakeyaml/issues/320/allow-subclasses-of-constructor-to-provide
         */
        static private class YamlHack
        {
            protected BaseConstructor constructor;
    
            YamlHack(BaseConstructor constructor)
            {
                this.constructor = constructor;
            }
    
            @SuppressWarnings("unchecked")
            public <T> T loadAs(InputStream input, Class<T> type, String defaultTag) {
                return (T) loadFromReader(new StreamReader(new UnicodeReader(input)), type, defaultTag);
            }
    
            @SuppressWarnings("unchecked")
            public <T> T loadAs(InputStream input, Class<T> type) {
                return loadAs(input, type, null);
            }
    
            private Object loadFromReader(StreamReader sreader, Class<?> type, final String defaultTag) {
                if (defaultTag != null)
                {
                    /*
                     * override document tag, if appropriate, before the constructor
                     * gets to it
                     */
                    Composer composer = new Composer(new ParserImpl(sreader), new Resolver()) {
                        @Override public Node getSingleNode()
                        {
                            Node node = super.getSingleNode();
                            if (node.getTag() == Tag.MAP)
                            {
                                node.setTag(new Tag(defaultTag));
                            }
                            return node;
                        }
                    };
                    constructor.setComposer(composer);
                }
                else
                {
                    Composer composer = new Composer(new ParserImpl(sreader), new Resolver());
                    constructor.setComposer(composer);
                }
                return constructor.getSingleData(Object.class);
            }        
        }
    
       ...
    
            FileInputStream fis = new FileInputStream(file);
            org.yaml.snakeyaml.constructor.Constructor constructor = new org.yaml.snakeyaml.constructor.Constructor(); 
            addTagTypeDescription(constructor, TemplateConfigurationV1.class, "!v1");
            addTagTypeDescription(constructor, TemplateConfigurationV2.class, "!v2");
            YamlHack yaml = new YamlHack(constructor);
            TemplateConfigurationFile tcfg = yaml.loadAs(fis, TemplateConfigurationFile.class, "!v1");
    
  3. Log in to comment