3

I'm trying to figure out how to skip serializing empty collections using YamlDotNet. I have experimented with both a custom ChainedObjectGraphVisitor and IYamlTypeConverter. I'm new to using YamlDotNet and have some knowledge gaps here.

Below is my implementation for the visitor pattern, which results in a YamlDotNet.Core.YamlException "Expected SCALAR, SEQUENCE-START, MAPPING-START, or ALIAS, got MappingEnd" error. I do see some online content for MappingStart/MappingEnd, but I'm not sure how it fits into what I'm trying to do (eliminate clutter from lots of empty collections). Any pointers in the right direction are appreciated.

Instantiating the serializer:

var serializer = new YamlDotNet.Serialization.SerializerBuilder()
                .WithNamingConvention(new YamlDotNet.Serialization.NamingConventions.CamelCaseNamingConvention())
                .WithEmissionPhaseObjectGraphVisitor(args => new YamlIEnumerableSkipEmptyObjectGraphVisitor(args.InnerVisitor))
                .Build();

ChainedObjectGraphVisitor implementation:

    public sealed class YamlIEnumerableSkipEmptyObjectGraphVisitor : ChainedObjectGraphVisitor
{
    public YamlIEnumerableSkipEmptyObjectGraphVisitor(IObjectGraphVisitor<IEmitter> nextVisitor)
        : base(nextVisitor)
    {
    }

    public override bool Enter(IObjectDescriptor value, IEmitter context)
    {
        bool retVal;

        if (typeof(System.Collections.IEnumerable).IsAssignableFrom(value.Value.GetType()))
        {   // We have a collection
            var enumerableObject = (System.Collections.IEnumerable)value.Value;
            if (enumerableObject.GetEnumerator().MoveNext()) // Returns true if the collection is not empty.
            {   // Serialize it as normal.
                retVal = base.Enter(value, context);
            }
            else
            {   // Skip this item.
                retVal = false;
            }
        }
        else
        {   // Not a collection, normal serialization.
            retVal = base.Enter(value, context);
        }

        return retVal;
    }
}
Paul Schroeder
  • 1,460
  • 1
  • 14
  • 21

3 Answers3

3

I believe the answer is to also override the EnterMapping() method in the base class with logic that is similar to what was done in the Enter() method:

public override bool EnterMapping(IPropertyDescriptor key, IObjectDescriptor value, IEmitter context)
{
    bool retVal = false;

    if (value.Value == null)
        return retVal;

    if (typeof(System.Collections.IEnumerable).IsAssignableFrom(value.Value.GetType()))
    {   // We have a collection
        var enumerableObject = (System.Collections.IEnumerable)value.Value;
        if (enumerableObject.GetEnumerator().MoveNext()) // Returns true if the collection is not empty.
        {   // Don't skip this item - serialize it as normal.
            retVal = base.EnterMapping(key, value, context);
        }
        // Else we have an empty collection and the initialized return value of false is correct.
    }
    else
    {   // Not a collection, normal serialization.
        retVal = base.EnterMapping(key, value, context);
    }

    return retVal;
}
Mariusz Jamro
  • 30,615
  • 24
  • 120
  • 162
Paul Schroeder
  • 1,460
  • 1
  • 14
  • 21
  • 1
    This is the recommended way to achieve your goal. The implementation of [DefaultExclusiveObjectGraphVisitor](https://github.com/aaubry/YamlDotNet/blob/master/YamlDotNet/Serialization/ObjectGraphVisitors/DefaultExclusiveObjectGraphVisitor.cs) uses a similar strategy to avoid emitting default values. – Antoine Aubry Jun 04 '19 at 21:39
3

you can specify DefaultValuesHandling

in the serializer:

var serializer = new SerializerBuilder()
    .ConfigureDefaultValuesHandling(DefaultValuesHandling.OmitEmptyCollections)
    .Build();   

or in an attribute YamlMember for a field/property:

public class MyDtoClass
{

    [YamlMember(DefaultValuesHandling = DefaultValuesHandling.OmitEmptyCollections)]
    public List<string> MyCollection;
}
msergey
  • 31
  • 1
  • 2
2

I ended up with the following class:

using System.Collections;
using YamlDotNet.Core;
using YamlDotNet.Serialization;
using YamlDotNet.Serialization.ObjectGraphVisitors;

sealed class YamlIEnumerableSkipEmptyObjectGraphVisitor : ChainedObjectGraphVisitor
{
    public YamlIEnumerableSkipEmptyObjectGraphVisitor(IObjectGraphVisitor<IEmitter> nextVisitor): base(nextVisitor)
    {
    }

    private bool IsEmptyCollection(IObjectDescriptor value)
    {
        if (value.Value == null)
            return true;

        if (typeof(IEnumerable).IsAssignableFrom(value.Value.GetType()))
            return !((IEnumerable)value.Value).GetEnumerator().MoveNext();

        return false;
    }

    public override bool Enter(IObjectDescriptor value, IEmitter context)
    {
        if (IsEmptyCollection(value))
            return false;

        return base.Enter(value, context);
    }

    public override bool EnterMapping(IPropertyDescriptor key, IObjectDescriptor value, IEmitter context)
    {
        if (IsEmptyCollection(value))
            return false;

        return base.EnterMapping(key, value, context);
    }
}

Kees C. Bakker
  • 32,294
  • 27
  • 115
  • 203