Десериализовать словарь<string, object> со значениями перечисления в C#

Я пытаюсь сериализовать / десериализовать Dictionary<string, object> в C#. Объект может быть любым, что можно сериализировать.

Json.NET почти работает, но если значение в словаре enumдесериализация не является правильной, поскольку она десериализована как long, TypeNameHandling.All не имеет никакого значения.

Есть ли другое быстрое решение для библиотеки сериализации. Результат не должен быть в формате JSON, но должен быть текстовым.

Я также не имею никакого влияния на данные, которые передаются в словарь. Я просто должен сериализовать и десериализовать все, что встречается на моем пути.

РЕДАКТИРОВАТЬ: StringEnumConverter не помогает. Данные преобразуются обратно в Dictionary<string, object>поэтому десериализатор не знает, что сериализованное значение является enum, Он рассматривает это как объект, с StringEnumConverter это остается string после десериализации; это десериализовано как long без конвертера. JSON.NET не сохраняет перечисление.

Решение, которое я хочу предоставить, представляет собой реализацию существующего интерфейса, который внедряется в существующее решение, которое я не могу изменить.

EDIT2: вот пример того, что я пытаюсь сделать

public enum Foo { A, B, C }
public enum Bar { A, B, C }
public class Misc { public Foo Foo { get; set; } }


var dict = new Dictionary<string, object>();
dict.Add("a", Foo.A);
dict.Add("b", Bar.B);
dict.Add("c", new Misc());

// serialize dict to a string s
// deserialize s to a Dictionary<string, object> dict2

Assert.AreEqual(Foo.A, dict2["a"]);
Assert.AreEqual(Bar.B, dict2["b"]);

Важно: я не могу контролировать dict; это на самом деле пользовательский тип, который является производным от Dictionary<string, object>Я просто должен убедиться, что все десериализованные ключи и значения принадлежат к одному и тому же типу при десериализации, так что приведения не требуется. И опять же, мне не нужно использовать JSON; может быть, есть какой-то другой сериализатор, который может справиться с работой!?

2 ответа

Решение

Предположительно, вы уже сериализуете свой словарь TypeNameHandling.All, который должен правильно сериализовать и десериализовать new Misc() значение путем испускания "$type" свойство объекта вместе с самим объектом. К сожалению, для типов, таких как enums (и других, таких как int а также long), это не работает, потому что они сериализуются как примитивы JSON, без возможности включить "$type" имущество.

Решение заключается в том, чтобы при сериализации словаря object значения, чтобы сериализовать обертки объекта для примитивных значений, которые могут инкапсулировать информацию о типе, в соответствии с этим ответом. Поскольку вы не можете изменить ни один из входящих объектов и вам нужно "внедрить" подходящие оболочки, вы можете сделать это с помощью специального средства распознавания контрактов, которое применяет соответствующий преобразователь элементов к значениям словаря:

public class UntypedToTypedValueContractResolver : DefaultContractResolver
{
    // As of 7.0.1, Json.NET suggests using a static instance for "stateless" contract resolvers, for performance reasons.
    // http://www.newtonsoft.com/json/help/html/ContractResolver.htm
    // http://www.newtonsoft.com/json/help/html/M_Newtonsoft_Json_Serialization_DefaultContractResolver__ctor_1.htm
    // "Use the parameterless constructor and cache instances of the contract resolver within your application for optimal performance."
    // See also https://stackru.com/questions/33557737/does-json-net-cache-types-serialization-information
    static UntypedToTypedValueContractResolver instance;

    // Explicit static constructor to tell C# compiler not to mark type as beforefieldinit
    static UntypedToTypedValueContractResolver() { instance = new UntypedToTypedValueContractResolver(); }

    public static UntypedToTypedValueContractResolver Instance { get { return instance; } }

    protected override JsonDictionaryContract CreateDictionaryContract(Type objectType)
    {
        var contract = base.CreateDictionaryContract(objectType);

        if (contract.DictionaryValueType == typeof(object) && contract.ItemConverter == null)
        {
            contract.ItemConverter = new UntypedToTypedValueConverter();
        }

        return contract;
    }
}

class UntypedToTypedValueConverter : JsonConverter
{
    public override bool CanConvert(Type objectType)
    {
        throw new NotImplementedException("This converter should only be applied directly via ItemConverterType, not added to JsonSerializer.Converters");
    }

    public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
    {
        if (reader.TokenType == JsonToken.Null)
            return null;
        var value = serializer.Deserialize(reader, objectType);
        if (value is TypeWrapper)
        {
            return ((TypeWrapper)value).ObjectValue;
        }
        return value;
    }

    public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
    {
        if (serializer.TypeNameHandling == TypeNameHandling.None)
        {
            Debug.WriteLine("ObjectItemConverter used when serializer.TypeNameHandling == TypeNameHandling.None");
            serializer.Serialize(writer, value);
        }
        // Handle a couple of simple primitive cases where a type wrapper is not needed
        else if (value is string)
        {
            writer.WriteValue((string)value);
        }
        else if (value is bool)
        {
            writer.WriteValue((bool)value);
        }
        else
        {
            var contract = serializer.ContractResolver.ResolveContract(value.GetType());
            if (contract is JsonPrimitiveContract)
            {
                var wrapper = TypeWrapper.CreateWrapper(value);
                serializer.Serialize(writer, wrapper, typeof(object));
            }
            else
            {
                serializer.Serialize(writer, value);
            }
        }
    }
}

public abstract class TypeWrapper
{
    protected TypeWrapper() { }

    [JsonIgnore]
    public abstract object ObjectValue { get; }

    public static TypeWrapper CreateWrapper<T>(T value)
    {
        if (value == null)
            return new TypeWrapper<T>();
        var type = value.GetType();
        if (type == typeof(T))
            return new TypeWrapper<T>(value);
        // Return actual type of subclass
        return (TypeWrapper)Activator.CreateInstance(typeof(TypeWrapper<>).MakeGenericType(type), value);
    }
}

public sealed class TypeWrapper<T> : TypeWrapper
{
    public TypeWrapper() : base() { }

    public TypeWrapper(T value)
        : base()
    {
        this.Value = value;
    }

    public override object ObjectValue { get { return Value; } }

    public T Value { get; set; }
}

Тогда используйте это как:

var settings = new JsonSerializerSettings
{
    TypeNameHandling = TypeNameHandling.All,
    ContractResolver = UntypedToTypedValueContractResolver.Instance,
    Converters = new [] { new StringEnumConverter() }, // If you prefer
};

var json = JsonConvert.SerializeObject(dict, Formatting.Indented, settings);

var dict2 = JsonConvert.DeserializeObject<Dictionary<string, object>>(json, settings);

Образец скрипки.

Наконец, при использовании TypeNameHandling, обратите внимание на это предостережение из документации Newtonsoft:

TypeNameHandling следует использовать с осторожностью, когда ваше приложение десериализует JSON из внешнего источника. Входящие типы должны проверяться с помощью настраиваемого SerializationBinder при десериализации со значением, отличным от None.

Для обсуждения того, почему это может быть необходимо, см. Предупреждение о TypeNameHandling в Newtonsoft Json.

Предполагая, что у вас есть доступ для изменения класса объекта, вы можете добавить атрибут JsonCoverter к члену перечисления класса.

[JsonConverter(typeof(StringEnumConverter))]
Другие вопросы по тегам