web-dev-qa-db-fra.com

Sérialisation null dans JSON.NET

Lors de la sérialisation de données arbitraires via JSON.NET, toute propriété qui est nulle est écrite dans le JSON en tant que

"propertyName": null

C'est correct, bien sûr.

Cependant, j'ai une obligation de traduire automatiquement toutes les valeurs nulles dans la valeur vide par défaut, par exemple null strings doit devenir String.Empty, null int? s doit devenir 0, null bool? s doit être false, etc.

NullValueHandling n'est pas utile, car je ne veux pas Ignore null, mais je ne veux pas non plus Include (Hmm, nouvelle fonctionnalité?).

Je me suis donc tourné vers l'implémentation d'un JsonConverter personnalisé.
Alors que l'implémentation elle-même était un jeu d'enfant, malheureusement cela n'a toujours pas fonctionné - CanConvert() n'est jamais appelé pour une propriété qui a une valeur nulle, et donc WriteJson() n'est pas appelé non plus. Apparemment, les valeurs nulles sont automatiquement sérialisées directement dans null, sans le pipeline personnalisé.

Par exemple, voici un exemple de convertisseur personnalisé pour les chaînes nulles:

public class StringConverter : JsonConverter
{
    public override bool CanConvert(Type objectType)
    {
        return typeof(string).IsAssignableFrom(objectType);
    }

    ...
    public override void WriteJson(JsonWriter writer, 
                object value, 
                JsonSerializer serializer)
    {
        string strValue = value as string;

        if (strValue == null)
        {
            writer.WriteValue(String.Empty);
        }
        else
        {
            writer.WriteValue(strValue);
        }
    }
}

En parcourant cela dans le débogueur, j'ai remarqué qu'aucune de ces méthodes n'est appelée pour les propriétés qui ont une valeur nulle.

En fouillant dans le code source de JSON.NET, j'ai trouvé que (apparemment, je ne suis pas allé beaucoup en profondeur) il y a un cas spécial vérifiant les valeurs nulles et appelant explicitement .WriteNull().

Pour ce que ça vaut, j'ai essayé d'implémenter un JsonTextWriter personnalisé et de remplacer l'implémentation par défaut .WriteNull() ...

public class NullJsonWriter : JsonTextWriter
{
    ... 
    public override void WriteNull()
    {
        this.WriteValue(String.Empty);
    }
}

Cependant, cela ne peut pas bien fonctionner, car la méthode WriteNull() ne sait rien du type de données sous-jacent. Donc, bien sûr, je peux sortir "" Pour n'importe quel null, mais cela ne fonctionne pas bien par exemple int, bool, etc.

Donc, ma question - à moins de convertir manuellement la totalité de la structure de données, existe-t-il une solution ou une solution pour cela?

46
AviD

D'accord, je pense que j'ai trouvé une solution (ma première solution n'était pas bonne du tout, mais là encore j'étais dans le train). Vous devez créer un résolveur de contrat spécial et un ValueProvider personnalisé pour les types Nullable. Considère ceci:

public class NullableValueProvider : IValueProvider
{
    private readonly object _defaultValue;
    private readonly IValueProvider _underlyingValueProvider;


    public NullableValueProvider(MemberInfo memberInfo, Type underlyingType)
    {
        _underlyingValueProvider = new DynamicValueProvider(memberInfo);
        _defaultValue = Activator.CreateInstance(underlyingType);
    }

    public void SetValue(object target, object value)
    {
        _underlyingValueProvider.SetValue(target, value);
    }

    public object GetValue(object target)
    {
        return _underlyingValueProvider.GetValue(target) ?? _defaultValue;
    }
}

public class SpecialContractResolver : DefaultContractResolver
{
    protected override IValueProvider CreateMemberValueProvider(MemberInfo member)
    {
        if(member.MemberType == MemberTypes.Property)
        {
            var pi = (PropertyInfo) member;
            if (pi.PropertyType.IsGenericType && pi.PropertyType.GetGenericTypeDefinition() == typeof (Nullable<>))
            {
                return new NullableValueProvider(member, pi.PropertyType.GetGenericArguments().First());
            }
        }
        else if(member.MemberType == MemberTypes.Field)
        {
            var fi = (FieldInfo) member;
            if(fi.FieldType.IsGenericType && fi.FieldType.GetGenericTypeDefinition() == typeof(Nullable<>))
                return new NullableValueProvider(member, fi.FieldType.GetGenericArguments().First());
        }

        return base.CreateMemberValueProvider(member);
    }
}

Ensuite, je l'ai testé en utilisant:

class Foo
{
    public int? Int { get; set; }
    public bool? Boolean { get; set; }
    public int? IntField;
}

Et le cas suivant:

[TestFixture]
public class Tests
{
    [Test]
    public void Test()
    {
        var foo = new Foo();

        var settings = new JsonSerializerSettings { ContractResolver = new SpecialContractResolver() };

        Assert.AreEqual(
            JsonConvert.SerializeObject(foo, Formatting.None, settings), 
            "{\"IntField\":0,\"Int\":0,\"Boolean\":false}");
    }
}

Espérons que cela aide un peu ...

Éditer - Meilleure identification d'un a Nullable<> type

Édition - Ajout de la prise en charge des champs ainsi que des propriétés, également superposition au-dessus du DynamicValueProvider normal pour faire la plupart du travail, avec un test mis à jour

26
J. Holmes