programing

Json.net을 사용하여 복합 키를 사용하여 사전을 직렬화할 수 없습니다.

sourcejob 2023. 2. 12. 10:22
반응형

Json.net을 사용하여 복합 키를 사용하여 사전을 직렬화할 수 없습니다.

커스텀 .net 타입을 키로 하는 사전이 있습니다.JSON.net 를 사용해 이 사전을 JSON 에 시리얼 하려고 하고 있습니다만, 시리얼화중에 키를 적절한 값으로 변환할 수 없습니다.

class ListBaseClass
{
    public String testA;
    public String testB;
}
-----
var details = new Dictionary<ListBaseClass, string>();
details.Add(new ListBaseClass { testA = "Hello", testB = "World" }, "Normal");
var results = Newtonsoft.Json.JsonConvert.SerializeObject(details);
var data = Newtonsoft.Json.JsonConvert.DeserializeObject<Dictionary<ListBaseClass, string>> results);

Give me --> "{\" JSon Serialization.ListBaseClass\:"정상"

그러나 사전에서 사용자 지정 유형을 값으로 지정하면 올바르게 작동합니다.

  var details = new Dictionary<string, ListBaseClass>();
  details.Add("Normal", new ListBaseClass { testA = "Hello", testB = "World" });
  var results = Newtonsoft.Json.JsonConvert.SerializeObject(details);
  var data = Newtonsoft.Json.JsonConvert.DeserializeObject<Dictionary<string, ListBaseClass>>(results);

이것은 --> "{\"Normal\":{\"testA\":"Hello\","testB\":"World\"}"를 부여합니다.

내가 Json.net의 한계에 도달했거나 잘못된 행동을 하고 있는지 누군가가 제안할 수 있습니까?

당신은 아마도 고든 빈이 제시한 답을 사용하고 싶지 않을 것이다.솔루션은 동작하지만 출력에 시리얼화된 문자열을 제공합니다.JSON을 사용하는 경우 문자열 표현이 아닌 개체의 JSON 표현을 정말로 원하기 때문에 이상적인 결과가 되지 않습니다.

예를 들어 고유한 그리드 점을 문자열과 연결하는 데이터 구조가 있다고 가정합니다.

class Point
{
    public int x { get; set; }
    public int y { get; set; }
}

public Dictionary<Point,string> Locations { get; set; };

TypeConverter 덮어쓰기를 사용하면 이 개체를 시리얼화할 때 이 개체의 문자열 표현을 얻을 수 있습니다.

"Locations": {
  "4,3": "foo",
  "3,4": "bar"
},

하지만 우리가 정말 원하는 건

"Locations": {
  { "x": 4, "y": 3 }: "foo",
  { "x": 3, "y": 4 }: "bar"
},

TypeConverter를 덮어쓰고 클래스를 serialize/deserialize하는 데는 몇 가지 문제가 있습니다.

먼저, 이것은 JSON이 아닙니다.다른 곳에서 시리얼라이즈 및 역시리얼라이즈를 처리하기 위해 추가 커스텀 로직을 작성해야 할 수도 있습니다.(클라이언트 레이어에서 Javascript를 사용하는 경우 등)

둘째, 이 개체를 사용하는 다른 모든 곳에서 이 문자열이 출력됩니다.이 문자열은 이전에 개체에 대해 올바르게 시리얼화되었습니다.

"GridCenterPoint": { "x": 0, "y": 0 },

이제 시리얼화 대상:

"GridCenterPoint": "0,0",

TypeConverter 포맷은 조금 제어할 수 있지만 오브젝트가 아닌 문자열로 렌더링된다는 사실에서 벗어날 수 없습니다.

Json부터 이 문제는 시리얼라이저에 문제가 없습니다.NET은 복잡한 오브젝트를 박자를 놓치지 않고 씹습니다.이것은 사전 키의 처리 방법의 문제입니다.예제 개체를 가져와서 목록 또는 해시 집합을 직렬화하려고 하면 올바른 JSON을 생성하는 데 문제가 없음을 알 수 있습니다.이것은 우리에게 이 문제를 해결하는 훨씬 더 간단한 방법을 제공한다.

진손NET: 키를 강제로 문자열로 만들지 않고 개체 유형에 관계없이 직렬화합니다.그건 선택사항이 아닌 것 같기 때문에 다른 방법은 Json을 주는 것입니다.할 수 " " " " " NET: "List<KeyValuePair<T,K>>.

KeyValuePair 목록을 Json에 공급하는 경우.NET의 시리얼 라이저라면 기대했던 대로 얻을 수 있습니다.예를 들어, 구현 가능한 훨씬 단순한 래퍼를 다음에 나타냅니다.

    private Dictionary<Point, string> _Locations;
    public List<KeyValuePair<Point, string>> SerializedLocations
    {
        get { return _Locations.ToList(); }
        set { _Locations= value.ToDictionary(x => x.Key, x => x.Value); }
    }

@comment:@bmw15bm update update update:
하고 [를 추가하여 [을 만들 수 .KeyValuePairs는 KeyValuePairs입니다.

이 트릭은 kvp의 키가 강제로 문자열 형식으로 되어 있지 않기 때문에 동작합니다.왜트트 맷맷??놀랍군요.에는 '천하태평'이 되어 있습니다. 이치노IEnumerable<KeyValuePair<TKey, TValue>>따라서 kvps 목록과 같은 방식으로 직렬화하는 데 문제가 없을 것입니다. 왜냐하면 그것이 본질적으로 사전이기 때문입니다.어떤 사람(제임스 뉴턴?)은 뉴턴소프트 사전 연재기를 쓸 때 복잡한 키를 다루기에는 너무 지저분하다고 결정을 내렸다.이 문제를 훨씬 더 끈적끈적한 문제로 만드는, 제가 미처 생각하지 못한 코너 케이스가 있을 것입니다.

이는 실제 JSON 객체를 생성하고 기술적으로 더 단순하며 시리얼라이저를 교체해도 부작용이 발생하지 않기 때문에 훨씬 더 나은 솔루션입니다.

시리얼라이제이션 가이드」에 기재되어 있습니다(섹션 참조).딕셔너리와 해시테이블.링크에 대해 @Shashwat 감사합니다).

사전을 직렬화할 때 사전의 키가 문자열로 변환되어 JSON 개체 속성 이름으로 사용됩니다.키에 대해 기술된 문자열은 키 유형에 대해 ToString()을 덮어쓰거나 TypeConverter를 구현하여 맞춤화할 수 있습니다.TypeConverter는 사전을 역직렬화할 때 커스텀 문자열의 재변환도 지원합니다.

Microsoft 의 「How-to」페이지에서, 이러한 타입 컨버터를 실장하는 방법에 관한 유용한 예를 찾았습니다.

  • 타입 컨버터를 실장합니다(값 변환을 위한 타입 컨버터 섹션 참조).

기본적으로, 저는 이 모든 것들을System.ComponentModel.TypeConverter★★★★★★★★★★★★★★★★★★:

bool CanConvertFrom(ITypeDescriptorContext context, Type source);

object ConvertFrom(ITypeDescriptorContext context,
                   System.Globalization.CultureInfo culture, object value);

object ConvertTo(ITypeDescriptorContext context, 
                 System.Globalization.CultureInfo culture, 
                 object value, Type destinationType);

또한 속성을 추가해야 했습니다. [TypeConverter(typeof(MyClassConverter))]에게MyClass클래스 선언

이것들 덕분에 사전을 자동으로 직렬화 및 역직렬화 할 수 있었습니다.

이를 위한 또 다른 방법은 커스텀 Contract Resolver를 사용하여 Override Creator를 설정하는 것입니다.

public class DictionaryAsArrayResolver : DefaultContractResolver
{
    public override JsonContract CreateContract(Type objectType)
    {
        if (IsDictionary(objectType))
        {
            JsonArrayContract contract = base.CreateArrayContract(objectType);
            contract.OverrideCreator = (args) => CreateInstance(objectType);
            return contract;
        }

        return base.CreateContract(objectType);
    }

    internal static bool IsDictionary(Type objectType)
    {
        if (objectType.IsGenericType && objectType.GetGenericTypeDefinition() == typeof(IDictionary<,>))
        {
            return true;
        }

        if (objectType.GetInterface(typeof(IDictionary<,>).Name) != null)
        {
            return true;
        }

        return false;
    }

    private object CreateInstance(Type objectType)
    {
        Type dictionaryType = typeof(Dictionary<,>).MakeGenericType(objectType.GetGenericArguments());
        return Activator.CreateInstance(dictionaryType);
    }
}

사용방법:

JsonSerializer jsonSerializer = new JsonSerializer();
jsonSerializer.ContractResolver = new DictionaryAsArrayResolver();

@roger-hill 답변 후 동일한 결과를 얻기 위해 경량 솔루션을 생각해 냈습니다.

    [JsonArray]
    public class MyDictionary<K, V> : Dictionary<K, V>
    {
    }

이런 식으로MyDictionary오브젝트는 키/값 쌍의 배열로 시리얼화되며 복잡한 키 유형에서도 올바르게 동작합니다.

[{
    "Key": ...,
    "Value": ...
}, ...]

gson에서 영감을 받아 complex Map Key Serialization과 그 기능\을 실행합니다.

public class DictionaryAsArrayJsonConverter : JsonConverter
{
    public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
    {
        var dictionary = (IDictionary)value;

        writer.WriteStartArray();

        var en = dictionary.GetEnumerator();
        while (en.MoveNext())
        {
            writer.WriteStartArray();
            serializer.Serialize(writer, en.Key);
            serializer.Serialize(writer, en.Value);
            writer.WriteEndArray();
        }
        
        writer.WriteEndArray();
    }

    public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
    {
        if (!CanConvert(objectType))
            throw new Exception(string.Format("This converter is not for {0}.", objectType));

        Type keyType = null;
        Type valueType = null;
        IDictionary result;

        if (objectType.IsGenericType)
        {
            keyType = objectType.GetGenericArguments()[0];
            valueType = objectType.GetGenericArguments()[1];
            var dictionaryType = typeof(Dictionary<,>).MakeGenericType(keyType, valueType);
            result = (IDictionary)Activator.CreateInstance(dictionaryType);
        }
        else
        {
            result = (IDictionary)Activator.CreateInstance(objectType);
        }

        if (reader.TokenType == JsonToken.Null)
            return null;

        int depth = reader.Depth;
        while (reader.Read())
        {
            if (reader.TokenType == JsonToken.StartArray)
            {
            }
            else if (reader.TokenType == JsonToken.EndArray)
            {
                if (reader.Depth == depth)
                    return result;
            }
            else
            {
                object key = serializer.Deserialize(reader, keyType);
                reader.Read();
                object value = serializer.Deserialize(reader, valueType);
                result.Add(key, value);
            }
        }

        return result;
    }

    public override bool CanConvert(Type objectType)
    {
        return typeof(IDictionary).IsAssignableFrom(objectType);
    }

}

Tal Aloni 코드와 동일한 json을 만들 수 있지만 계약이 아닌 JsonConverter로 만들 수 있습니다.JsonConverterAttribute를 사용하여 선택한 속성 또는 JsonSerializerSettings를 사용하여 모든 속성에서 사용할 수 있으므로 유연성이 향상됩니다.컨버터추가(...)

@roger-hill의 통찰력 있는 대응을 바탕으로 다음과 같이 작성했습니다.JsonConverter변환하다IDictionary에 반대하다.ListKeyValuePair물건들.

기트허브 링크

public class ListDictionaryConverter : JsonConverter
{
    private static (Type kvp, Type list, Type enumerable, Type[] args) GetTypes(Type objectType)
    {
        var args = objectType.GenericTypeArguments;
        var kvpType = typeof(KeyValuePair<,>).MakeGenericType(args);
        var listType = typeof(List<>).MakeGenericType(kvpType);
        var enumerableType = typeof(IEnumerable<>).MakeGenericType(kvpType);

        return (kvpType, listType, enumerableType, args);
    }

    public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
    {
        var (kvpType, listType, _, args) = GetTypes(value.GetType());
        
        var keys = ((IDictionary)value).Keys.GetEnumerator();
        var values = ((IDictionary)value).Values.GetEnumerator();
        var cl = listType.GetConstructor(Array.Empty<Type>());
        var ckvp = kvpType.GetConstructor(args);
        
        var list = (IList)cl!.Invoke(Array.Empty<object>());
        while (keys.MoveNext() && values.MoveNext())
        {
            list.Add(ckvp!.Invoke(new []{keys.Current, values.Current}));
        }
        
        serializer.Serialize(writer, list);
    }

    public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
    {
        var (_, listType, enumerableType, args) = GetTypes(objectType);
        
        var list = ((IList)(serializer.Deserialize(reader, listType)));

        var ci = objectType.GetConstructor(new[] {enumerableType});
        if (ci == null)
        {
            ci = typeof(Dictionary<,>).MakeGenericType(args).GetConstructor(new[] {enumerableType});
        }
        
        var dict = (IDictionary) ci!.Invoke(new object[]{ list });

        return dict;
    }

    public override bool CanConvert(Type objectType)
    {
        if (!objectType.IsGenericType) return objectType.IsAssignableTo(typeof(IDictionary));
        
        var args = objectType.GenericTypeArguments;
        return args.Length == 2 && objectType.IsAssignableTo(typeof(IDictionary<,>).MakeGenericType(args));
    }
}

몇 가지 테스트를 해봤는데 이 코드는 테스트에서 잘 작동합니다.엣지 케이스를 한두 개 놓쳤나 봐요

모든 것이 쉬워졌다

var details = new Dictionary<string, ListBaseClass>();
details.Add("Normal", new ListBaseClass { testA = "Hello", testB = "World" });
var results = Newtonsoft.Json.JsonConvert.SerializeObject(details.ToList());
var data = 
Newtonsoft.Json.JsonConvert.DeserializeObject<Dictionary<ListBaseClass, string>> results);

샘플

 class Program
{
    static void Main(string[] args)
    {
        var  testDictionary = new Dictionary<TestKey,TestValue>()
        {
            {
                new TestKey()
                {
                    TestKey1 = "1",
                    TestKey2 = "2",
                    TestKey5 = 5
                },
                new TestValue()
                {
                    TestValue1 = "Value",
                    TestValue5 = 96
                }
            }
        };

        var json = JsonConvert.SerializeObject(testDictionary);
        Console.WriteLine("=== Dictionary<TestKey,TestValue> ==");
        Console.WriteLine(json);
        // result: {"ConsoleApp2.TestKey":{"TestValue1":"Value","TestValue5":96}}


        json = JsonConvert.SerializeObject(testDictionary.ToList());
        Console.WriteLine("=== List<KeyValuePair<TestKey, TestValue>> ==");
        Console.WriteLine(json);
        // result: [{"Key":{"TestKey1":"1","TestKey2":"2","TestKey5":5},"Value":{"TestValue1":"Value","TestValue5":96}}]


        Console.ReadLine();

    }
}

class TestKey
{
    public string TestKey1 { get; set; }

    public string TestKey2 { get; set; }

    public int TestKey5 { get; set; }
}

class TestValue 
{
    public string TestValue1 { get; set; }

    public int TestValue5 { get; set; }
}

언급URL : https://stackoverflow.com/questions/24504245/not-ableto-serialize-dictionary-with-complex-key-using-json-net

반응형