YamlDotNet icon indicating copy to clipboard operation
YamlDotNet copied to clipboard

"!" should not be a shorthand for "tag:yaml.org,2002:"

Open aaubry opened this issue 8 years ago • 4 comments

  • http://www.yaml.org/spec/1.2/spec.html#id2782457
  • http://stackoverflow.com/questions/38248697/how-to-serialize-deserialize-list-with-interface/38250533?noredirect=1#comment-72373677

aaubry avatar Mar 07 '17 15:03 aaubry

Deducing from the behavior of my code I think that this is already fixed. At least I need !! for correct deserialization.

What still seems to be missing is the serialization counterpart which AFAIK makes roundtripping interfaces impossible (and gives an error if you add .EnsureRoundtrip() to the serializer).

Brar avatar Nov 03 '19 09:11 Brar

Could you elaborate? What's not working? Thanks

aaubry avatar Nov 03 '19 15:11 aaubry

The following is a self contained example including comments that you can simply copy and paste.

Mind you, that I already use !! as shorthand, which this issue is about.

IMHO this means that this issue can probably be closed but that there is still the problem that the serializer doesn't know about tags.

using System;
using YamlDotNet.Serialization;
using YamlDotNet.Serialization.NamingConventions;

namespace YamlDotNetDemo
{
    class Program
    {
        private static readonly IDeserializer Deserializer = new DeserializerBuilder()
            .WithNamingConvention(CamelCaseNamingConvention.Instance)
            .WithTagMapping("tag:yaml.org,2002:dog", typeof(Dog))
            .WithTagMapping("tag:yaml.org,2002:fish", typeof(Fish))
            .Build();

        private static readonly ISerializer Serializer = new SerializerBuilder()
            .WithNamingConvention(CamelCaseNamingConvention.Instance)
            //.EnsureRoundtrip()
            .ConfigureDefaultValuesHandling(DefaultValuesHandling.OmitDefaults)
            .Build();

        static void Main()
        {
            var input = @"- name: Adam
  ownedPets:
  - !!dog
    furColor: Brown
    name: Barney
  - !!fish
    name: Casper
- name: Dora
  ownedPets:
  - !!fish
    saltWater: true
    name: Drake";

            Console.WriteLine($"Original:\n\"{input}\"");
            var deserialized = Deserializer.Deserialize<Person[]>(input);

            // This will already fail if you uncomment .EnsureRoundtrip() above
            // because no tag mapping has been registered for the serializer
            // which AFAIK isn't possible ATM.
            var serialized = Serializer.Serialize(deserialized);
            Console.WriteLine($"Roundtripped:\n\"{serialized}\"");

            // This finally fails because YamlDotNet can't create an instance of an interface
            // and it has lost the information (tags) while serializing.
            var deserialized2 = Deserializer.Deserialize<Person[]>(serialized);
        }
    }

    public interface IPet
    {
        string Name { get; set; }
    }

    public class Dog : IPet
    {
        public string FurColor { get; set; }
        public string Name { get; set; }
    }

    public class Fish : IPet
    {
        public bool SaltWater { get; set; }
        public string Name { get; set; }
    }

    public class Person
    {
        public string Name { get; set; }
        public IPet[] OwnedPets { get; set; }
    }
}

Brar avatar Nov 03 '19 17:11 Brar

After spending a whole day on this I was able to get a very similar scenario to work.

In my case I have a list of interface instead of an array. I have not tried it with array, but if it doesn't work with array just use a list instead.

If you have full control over classes that need to be serialized, my solution can be further improved by adding custom class attribute to help narrow down which types to include in mapping. In fact you could then indicate desired tag in the attribute instead of using type name if you'd like.

Add the following YamlSerializer wrapper class to you project:

using Eliza.Cdk.SiteSchema.Core.Utils;
using System;
using System.Collections.Generic;
using YamlDotNet.Serialization;
using YamlDotNet.Serialization.NamingConventions;

namespace Eliza.Cdk.SiteSchema.Core.Serialization
{
    public class YamlSerializer<T>
    {
        private static Dictionary<string, Type> TypeMap { get; set; }

        public string Serialize(T obj)
        {
            SerializerBuilder serializerBuilder = new SerializerBuilder();

            serializerBuilder = RegisterTypeMappings(serializerBuilder);

            ISerializer serializer = serializerBuilder
                                            .WithNamingConvention(PascalCaseNamingConvention.Instance)
                                            .EnsureRoundtrip()
                                            .Build();

            string yaml = serializer.Serialize(obj);
            return yaml;
        }

        public T Deserialize(string yaml)
        {
            DeserializerBuilder deserializerBuilder = new DeserializerBuilder();

            deserializerBuilder = RegisterTypeMappings(deserializerBuilder);

            IDeserializer deserializer = deserializerBuilder.Build();

            return deserializer.Deserialize<T>(yaml);
        }

        TBuilder RegisterTypeMappings<TBuilder>(TBuilder yamlBuilder) where TBuilder : BuilderSkeleton<TBuilder>
        {
            if (TypeMap == null)
            {
                GenerateTypeMap();
            }

            if (yamlBuilder is DeserializerBuilder)
            {
                TypeMap.ForEach(kvp =>
                    yamlBuilder = yamlBuilder.WithTagMapping($"tag:yaml.org,2002:{kvp.Key}", kvp.Value));
            }
            else
            {
                TypeMap.ForEach(kvp =>
                    yamlBuilder = yamlBuilder.WithTagMapping($"!!{kvp.Key}", kvp.Value));
            }

            return yamlBuilder;
        }

        void GenerateTypeMap()
        {
            TypeMap = new Dictionary<string, Type>();

            foreach (Type type in System.Reflection.Assembly.GetExecutingAssembly().GetTypes())
            {
                if (type.IsClass
                    && !type.IsAbstract
                    && !(type.IsAbstract && type.IsSealed)
                )
                {
                    if (type.IsNested && type.DeclaringType != null)
                    {
                        TypeMap[type.DeclaringType.Name] = type.DeclaringType;
                    }
                    else
                    {
                        TypeMap[type.Name] = type;
                    }
                }
            }
        }
    }
}

Then add serialization methods to your top level object (Person) like so:

        public string SerializeToYaml()
        {
            YamlSerializer<Person> yamlSerializer = new YamlSerializer<Person>();
            return yamlSerializer.Serialize(this);
        }

        public static SiteConfiguration DeserializeFromYaml(string yaml)
        {
            YamlSerializer<Person> yamlSerializer = new YamlSerializer<Person>();
            return yamlSerializer.Deserialize(yaml);
        }

lorlik avatar Jul 03 '20 02:07 lorlik

This has at some point been resolved. Add the following to your serializer builder

            .WithTagMapping("tag:yaml.org,2002:dog", typeof(Dog))
            .WithTagMapping("tag:yaml.org,2002:fish", typeof(Fish))

EdwardCooke avatar Jan 13 '23 20:01 EdwardCooke