วิธีการนำ JsonConverter แบบกำหนดเองไปใช้ใน JSON.NET เพื่อทำการ deserialize List ของออบเจ็กต์คลาสพื้นฐาน


301

ฉันพยายามขยายตัวอย่าง JSON.net ที่ให้ไว้ที่นี่ http://james.newtonking.com/projects/json/help/CustomCreationConverter.html

ฉันมีคลาสย่อยอื่นที่ได้มาจากคลาสฐาน / ส่วนต่อประสาน

public class Person
{
    public string FirstName { get; set; }
    public string LastName { get; set; }
}

public class Employee : Person
{
    public string Department { get; set; }
    public string JobTitle { get; set; }
}

public class Artist : Person
{
    public string Skill { get; set; }
}

List<Person> people  = new List<Person>
{
    new Employee(),
    new Employee(),
    new Artist(),
};

ฉันจะยกเลิกการติดตาม Json กลับไปที่รายการ <บุคคล> ได้อย่างไร

[
  {
    "Department": "Department1",
    "JobTitle": "JobTitle1",
    "FirstName": "FirstName1",
    "LastName": "LastName1"
  },
  {
    "Department": "Department2",
    "JobTitle": "JobTitle2",
    "FirstName": "FirstName2",
    "LastName": "LastName2"
  },
  {
    "Skill": "Painter",
    "FirstName": "FirstName3",
    "LastName": "LastName3"
  }
]

ฉันไม่ต้องการใช้ TypeNameHandling JsonSerializerSettings ฉันกำลังมองหาการติดตั้ง JsonConverter แบบกำหนดเองโดยเฉพาะเพื่อจัดการสิ่งนี้ เอกสารและตัวอย่างเกี่ยวกับเรื่องนี้ค่อนข้างกระจัดกระจายบนเน็ต ฉันไม่สามารถรับการใช้งานเมธอด ReadJson () ที่ overridden ใน JsonConverter ได้


คำตอบ:


315

การใช้มาตรฐานCustomCreationConverterฉันพยายามหาวิธีสร้างประเภทที่ถูกต้อง ( PersonหรือEmployee) เนื่องจากเพื่อกำหนดสิ่งนี้คุณต้องวิเคราะห์ JSON และไม่มีวิธีการสร้างขึ้นโดยใช้Createวิธีนี้

ฉันพบหัวข้อสนทนาที่เกี่ยวข้องกับการแปลงประเภทและมันก็เปิดออกเพื่อให้คำตอบ นี่คือลิงค์: ประเภทการแปลง

สิ่งที่จำเป็นคือการ subclass JsonConverterเอาชนะReadJsonวิธีการและการสร้างนามธรรมใหม่วิธีการที่ยอมรับCreateJObject

คลาส JObject จัดให้มีวิธีการโหลดวัตถุ JSON และให้การเข้าถึงข้อมูลภายในวัตถุนี้

ReadJsonเมธอดoverridden สร้าง a JObjectและเรียกใช้Createเมธอด (นำไปใช้โดยคลาสตัวแปลงที่เราได้รับ), ส่งผ่านในJObjectอินสแตนซ์

JObjectตัวอย่างนี้สามารถวิเคราะห์เพื่อกำหนดประเภทที่ถูกต้องโดยการตรวจสอบการมีอยู่ของเขตข้อมูลบางอย่าง

ตัวอย่าง

string json = "[{
        \"Department\": \"Department1\",
        \"JobTitle\": \"JobTitle1\",
        \"FirstName\": \"FirstName1\",
        \"LastName\": \"LastName1\"
    },{
        \"Department\": \"Department2\",
        \"JobTitle\": \"JobTitle2\",
        \"FirstName\": \"FirstName2\",
        \"LastName\": \"LastName2\"
    },
        {\"Skill\": \"Painter\",
        \"FirstName\": \"FirstName3\",
        \"LastName\": \"LastName3\"
    }]";

List<Person> persons = 
    JsonConvert.DeserializeObject<List<Person>>(json, new PersonConverter());

...

public class PersonConverter : JsonCreationConverter<Person>
{
    protected override Person Create(Type objectType, JObject jObject)
    {
        if (FieldExists("Skill", jObject))
        {
            return new Artist();
        }
        else if (FieldExists("Department", jObject))
        {
            return new Employee();
        }
        else
        {
            return new Person();
        }
    }

    private bool FieldExists(string fieldName, JObject jObject)
    {
        return jObject[fieldName] != null;
    }
}

public abstract class JsonCreationConverter<T> : JsonConverter
{
    /// <summary>
    /// Create an instance of objectType, based properties in the JSON object
    /// </summary>
    /// <param name="objectType">type of object expected</param>
    /// <param name="jObject">
    /// contents of JSON object that will be deserialized
    /// </param>
    /// <returns></returns>
    protected abstract T Create(Type objectType, JObject jObject);

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

    public override bool CanWrite
    {
        get { return false; }
    }

    public override object ReadJson(JsonReader reader, 
                                    Type objectType, 
                                     object existingValue, 
                                     JsonSerializer serializer)
    {
        // Load JObject from stream
        JObject jObject = JObject.Load(reader);

        // Create target object based on JObject
        T target = Create(objectType, jObject);

        // Populate the object properties
        serializer.Populate(jObject.CreateReader(), target);

        return target;
    }
}

6
มันจะดีถ้ามีการใช้วิธี WriteJson เช่นกันและเพื่อให้วิธีนามธรรมบางอย่างสำหรับ stringifying ชนิด
Triynko

54
หมายเหตุ: โซลูชั่นนี้มีอยู่ทั่วอินเทอร์เน็ต แต่มีข้อบกพร่องที่ปรากฏตัวในบางโอกาส ใหม่JsonReaderสร้างขึ้นในReadJsonวิธีการที่ไม่ได้รับมรดกใด ๆ ของการกำหนดค่าผู้อ่านต้นฉบับ ( Culture, DateParseHandling, DateTimeZoneHandling, FloatParseHandlingฯลฯ ... ) ค่าเหล่านี้ควรได้รับการคัดลอกไปก่อนที่จะใช้ใหม่ในJsonReader serializer.Populate()
Alain

9
เพื่อป้องกันไม่ให้สร้าง JsonReader ใหม่ (เนื่องจากเหตุผลดังกล่าวโดย @Alain) หรือถ้าคุณจะต้องตัดสินใจเกี่ยวกับประเภทของวัตถุที่สร้างขึ้นบนพื้นฐานของมูลค่าของผู้ปกครองให้ดูที่การแก้ปัญหานี้stackoverflow.com/a/22539730/1038496 ดูเหมือนจะมีประสิทธิภาพและชัดเจนกว่าสำหรับฉัน (แม้สำหรับปัญหาแบบนี้)
Zoka

8
@Triynko: หลังจากที่เวลานานของการค้นหาผมพบว่าชั้นJsonConverterมีคุณสมบัติที่เรียกว่าและCanRead CanWriteถ้าคุณไม่จำเป็นต้องกำหนดเองWriteJsonการดำเนินงานก็พอที่จะให้ผลตอบแทนCanWrite FALSEจากนั้นระบบจะถอยกลับไปสู่การทำงานเริ่มต้น @jdavies: โปรดเพิ่มเข้าไปในคำตอบของคุณ มิฉะนั้นจะมีปัญหาในการทำให้เป็นอันดับ
SimonSimCity

1
ฉันพบว่าคุณต้องจัดการกับกรณี NULL มิฉะนั้นจะเกิดข้อผิดพลาดที่ดี ใช้: ||| if (reader.TokenType == JsonToken.Null) คืนค่า null; |||| แหล่งที่มา: stackoverflow.com/a/34185296/857291
Cesar

96

วิธีการแก้ปัญหาข้างต้นสำหรับJsonCreationConverter<T>ทั่วอินเทอร์เน็ต แต่มีข้อบกพร่องที่ปรากฏตัวในโอกาสที่หายาก JsonReader ใหม่ที่สร้างในวิธี ReadJson ไม่ได้รับค่าการกำหนดค่าของผู้อ่านดั้งเดิม (วัฒนธรรม, DateParseHandling, DateTimeZoneHandling, FloatParseHandling, ฯลฯ ... ) ควรคัดลอกค่าเหล่านี้ก่อนใช้ JsonReader ใหม่ใน serializer.Populate ()

นี่คือสิ่งที่ดีที่สุดที่ฉันสามารถหาเพื่อแก้ไขปัญหาบางอย่างของการใช้งานข้างต้น แต่ฉันยังคิดว่ามีบางสิ่งที่ถูกมองข้าม:

อัปเดตฉันอัปเดตสิ่งนี้เพื่อให้มีวิธีที่ชัดเจนยิ่งขึ้นซึ่งทำสำเนาของผู้อ่านที่มีอยู่ นี่เป็นเพียงกระบวนการสรุปการคัดลอกการตั้งค่า JsonReader แต่ละรายการ เป็นการดีที่ฟังก์ชั่นนี้จะคงอยู่ในไลบรารี Newtonsoft แต่ตอนนี้คุณสามารถใช้สิ่งต่อไปนี้:

/// <summary>Creates a new reader for the specified jObject by copying the settings
/// from an existing reader.</summary>
/// <param name="reader">The reader whose settings should be copied.</param>
/// <param name="jToken">The jToken to create a new reader for.</param>
/// <returns>The new disposable reader.</returns>
public static JsonReader CopyReaderForObject(JsonReader reader, JToken jToken)
{
    JsonReader jTokenReader = jToken.CreateReader();
    jTokenReader.Culture = reader.Culture;
    jTokenReader.DateFormatString = reader.DateFormatString;
    jTokenReader.DateParseHandling = reader.DateParseHandling;
    jTokenReader.DateTimeZoneHandling = reader.DateTimeZoneHandling;
    jTokenReader.FloatParseHandling = reader.FloatParseHandling;
    jTokenReader.MaxDepth = reader.MaxDepth;
    jTokenReader.SupportMultipleContent = reader.SupportMultipleContent;
    return jTokenReader;
}

สิ่งนี้ควรใช้ดังนี้:

public override object ReadJson(JsonReader reader,
                                Type objectType,
                                object existingValue,
                                JsonSerializer serializer)
{
    if (reader.TokenType == JsonToken.Null)
        return null;
    // Load JObject from stream
    JObject jObject = JObject.Load(reader);
    // Create target object based on JObject
    T target = Create(objectType, jObject);
    // Populate the object properties
    using (JsonReader jObjectReader = CopyReaderForObject(reader, jObject))
    {
        serializer.Populate(jObjectReader, target);
    }
    return target;
}

โซลูชันที่เก่ากว่ามีดังนี้:

/// <summary>Base Generic JSON Converter that can help quickly define converters for specific types by automatically
/// generating the CanConvert, ReadJson, and WriteJson methods, requiring the implementer only to define a strongly typed Create method.</summary>
public abstract class JsonCreationConverter<T> : JsonConverter
{
    /// <summary>Create an instance of objectType, based properties in the JSON object</summary>
    /// <param name="objectType">type of object expected</param>
    /// <param name="jObject">contents of JSON object that will be deserialized</param>
    protected abstract T Create(Type objectType, JObject jObject);

    /// <summary>Determines if this converted is designed to deserialization to objects of the specified type.</summary>
    /// <param name="objectType">The target type for deserialization.</param>
    /// <returns>True if the type is supported.</returns>
    public override bool CanConvert(Type objectType)
    {
        // FrameWork 4.5
        // return typeof(T).GetTypeInfo().IsAssignableFrom(objectType.GetTypeInfo());
        // Otherwise
        return typeof(T).IsAssignableFrom(objectType);
    }

    /// <summary>Parses the json to the specified type.</summary>
    /// <param name="reader">Newtonsoft.Json.JsonReader</param>
    /// <param name="objectType">Target type.</param>
    /// <param name="existingValue">Ignored</param>
    /// <param name="serializer">Newtonsoft.Json.JsonSerializer to use.</param>
    /// <returns>Deserialized Object</returns>
    public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
    {
        if (reader.TokenType == JsonToken.Null)
            return null;

        // Load JObject from stream
        JObject jObject = JObject.Load(reader);

        // Create target object based on JObject
        T target = Create(objectType, jObject);

        //Create a new reader for this jObject, and set all properties to match the original reader.
        JsonReader jObjectReader = jObject.CreateReader();
        jObjectReader.Culture = reader.Culture;
        jObjectReader.DateParseHandling = reader.DateParseHandling;
        jObjectReader.DateTimeZoneHandling = reader.DateTimeZoneHandling;
        jObjectReader.FloatParseHandling = reader.FloatParseHandling;

        // Populate the object properties
        serializer.Populate(jObjectReader, target);

        return target;
    }

    /// <summary>Serializes to the specified type</summary>
    /// <param name="writer">Newtonsoft.Json.JsonWriter</param>
    /// <param name="value">Object to serialize.</param>
    /// <param name="serializer">Newtonsoft.Json.JsonSerializer to use.</param>
    public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
    {
        serializer.Serialize(writer, value);
    }
}

7
อย่าลืมนึกถึง CanWrite! (ฉันตั้งค่าเป็นเท็จ) คุณอาจจบลงด้วยการรายงานตัวเอง (ฉันทำ) stackoverflow.com/questions/12314438/…
Dribbel

1
คุณไม่จำเป็นต้องใช้งาน WriteJson หรือไม่ ตัวแปลงรู้วิธีแปลงจากวัตถุเป็น json ได้อย่างไร
David S.

15

แค่คิดว่าฉันจะแบ่งปันวิธีแก้ปัญหาตามนี้ซึ่งทำงานร่วมกับแอตทริบิวต์ Knowntype โดยใช้การสะท้อนต้องได้รับคลาสที่ได้รับจากคลาสพื้นฐานใด ๆ โซลูชันสามารถได้รับประโยชน์จากการเรียกซ้ำเพื่อหาคลาสการจับคู่ที่ดีที่สุด แต่ฉันไม่ต้องการมัน กรณีการจับคู่จะกระทำโดยชนิดที่กำหนดให้ตัวแปลงถ้ามี KnownTypes มันจะสแกนพวกเขาทั้งหมดจนกว่าจะตรงกับประเภทที่มีคุณสมบัติทั้งหมดภายในสตริง json จะเลือกตัวเลือกแรกที่ตรงกัน

การใช้งานง่ายพอ ๆ กับ:

 string json = "{ Name:\"Something\", LastName:\"Otherthing\" }";
 var ret  = JsonConvert.DeserializeObject<A>(json, new KnownTypeConverter());

ในกรณีข้างต้น ret จะเป็นประเภท B

คลาส JSON:

[KnownType(typeof(B))]
public class A
{
   public string Name { get; set; }
}

public class B : A
{
   public string LastName { get; set; }
}

รหัสแปลง:

/// <summary>
    /// Use KnownType Attribute to match a divierd class based on the class given to the serilaizer
    /// Selected class will be the first class to match all properties in the json object.
    /// </summary>
    public  class KnownTypeConverter : JsonConverter
    {
        public override bool CanConvert(Type objectType)
        {
            return System.Attribute.GetCustomAttributes(objectType).Any(v => v is KnownTypeAttribute);
        }

        public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
        {
            // Load JObject from stream
            JObject jObject = JObject.Load(reader);

            // Create target object based on JObject
            System.Attribute[] attrs = System.Attribute.GetCustomAttributes(objectType);  // Reflection. 

                // Displaying output. 
            foreach (System.Attribute attr in attrs)
            {
                if (attr is KnownTypeAttribute)
                {
                    KnownTypeAttribute k = (KnownTypeAttribute) attr;
                    var props = k.Type.GetProperties();
                    bool found = true;
                    foreach (var f in jObject)
                    {
                        if (!props.Any(z => z.Name == f.Key))
                        {
                            found = false;
                            break;
                        }
                    }

                    if (found)
                    {
                        var target = Activator.CreateInstance(k.Type);
                        serializer.Populate(jObject.CreateReader(),target);
                        return target;
                    }
                }
            }
            throw new ObjectNotFoundException();


            // Populate the object properties

        }

        public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
        {
            throw new NotImplementedException();
        }
    }

1
ฉันชอบวิธีนี้มาก แต่ฉันเห็นปัญหาเมื่อมีหลายประเภทที่รู้จักซึ่งมีชื่อคุณสมบัติที่แน่นอนเหมือนกัน คุณพบปัญหาดังกล่าวหรือไม่? ขอบคุณ.
covo

8

โครงการJsonSubTypesใช้ตัวแปลงทั่วไปที่จัดการคุณลักษณะนี้ด้วยความช่วยเหลือของคุณลักษณะ

สำหรับตัวอย่างคอนกรีตที่มีให้ที่นี่คือวิธีการทำงาน:

    [JsonConverter(typeof(JsonSubtypes))]
    [JsonSubtypes.KnownSubTypeWithProperty(typeof(Employee), "JobTitle")]
    [JsonSubtypes.KnownSubTypeWithProperty(typeof(Artist), "Skill")]
    public class Person
    {
        public string FirstName { get; set; }
        public string LastName { get; set; }
    }

    public class Employee : Person
    {
        public string Department { get; set; }
        public string JobTitle { get; set; }
    }

    public class Artist : Person
    {
        public string Skill { get; set; }
    }

    [TestMethod]
    public void Demo()
    {
        string json = "[{\"Department\":\"Department1\",\"JobTitle\":\"JobTitle1\",\"FirstName\":\"FirstName1\",\"LastName\":\"LastName1\"}," +
                      "{\"Department\":\"Department1\",\"JobTitle\":\"JobTitle1\",\"FirstName\":\"FirstName1\",\"LastName\":\"LastName1\"}," +
                      "{\"Skill\":\"Painter\",\"FirstName\":\"FirstName1\",\"LastName\":\"LastName1\"}]";


        var persons = JsonConvert.DeserializeObject<IReadOnlyCollection<Person>>(json);
        Assert.AreEqual("Painter", (persons.Last() as Artist)?.Skill);
    }

2
ตัวแปลงที่มีประโยชน์มาก เพิ่งบันทึกชั่วโมงของฉันในการเขียนโปรแกรมแปลงไฟล์ด้วยตัวเอง!
Carlos Rodriguez

7

นี่คือการขยายคำตอบของโทเท็ม มันเป็นพื้นเดียวกัน แต่การจับคู่คุณสมบัติขึ้นอยู่กับวัตถุ json ต่อเนื่องไม่สะท้อนวัตถุ. net นี่เป็นสิ่งสำคัญหากคุณกำลังใช้ [JsonProperty] โดยใช้ CamelCasePropertyNamesContractResolver หรือทำสิ่งอื่นที่จะทำให้ json ไม่ตรงกับวัตถุ. net

การใช้งานง่าย:

[KnownType(typeof(B))]
public class A
{
   public string Name { get; set; }
}

public class B : A
{
   public string LastName { get; set; }
}

รหัสแปลง:

/// <summary>
/// Use KnownType Attribute to match a divierd class based on the class given to the serilaizer
/// Selected class will be the first class to match all properties in the json object.
/// </summary>
public class KnownTypeConverter : JsonConverter {
    public override bool CanConvert( Type objectType ) {
        return System.Attribute.GetCustomAttributes( objectType ).Any( v => v is KnownTypeAttribute );
    }

    public override bool CanWrite {
        get { return false; }
    }

    public override object ReadJson( JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer ) {
        // Load JObject from stream
        JObject jObject = JObject.Load( reader );

        // Create target object based on JObject
        System.Attribute[ ] attrs = System.Attribute.GetCustomAttributes( objectType );  // Reflection. 

        // check known types for a match. 
        foreach( var attr in attrs.OfType<KnownTypeAttribute>( ) ) {
            object target = Activator.CreateInstance( attr.Type );

            JObject jTest;
            using( var writer = new StringWriter( ) ) {
                using( var jsonWriter = new JsonTextWriter( writer ) ) {
                    serializer.Serialize( jsonWriter, target );
                    string json = writer.ToString( );
                    jTest = JObject.Parse( json );
                }
            }

            var jO = this.GetKeys( jObject ).Select( k => k.Key ).ToList( );
            var jT = this.GetKeys( jTest ).Select( k => k.Key ).ToList( );

            if( jO.Count == jT.Count && jO.Intersect( jT ).Count( ) == jO.Count ) {
                serializer.Populate( jObject.CreateReader( ), target );
                return target;
            }
        }

        throw new SerializationException( string.Format( "Could not convert base class {0}", objectType ) );
    }

    public override void WriteJson( JsonWriter writer, object value, JsonSerializer serializer ) {
        throw new NotImplementedException( );
    }

    private IEnumerable<KeyValuePair<string, JToken>> GetKeys( JObject obj ) {
        var list = new List<KeyValuePair<string, JToken>>( );
        foreach( var t in obj ) {
            list.Add( t );
        }
        return list;
    }
}

5

ในฐานะที่เป็นรูปแบบอื่นในโซลูชันประเภทที่รู้จักของ Totem คุณสามารถใช้การสะท้อนเพื่อสร้างตัวจำแนกประเภททั่วไปเพื่อหลีกเลี่ยงความต้องการใช้แอตทริบิวต์ประเภทที่รู้จัก

วิธีนี้ใช้เทคนิคคล้ายกับJuval Lowy GenericResolverสำหรับ WCF

ตราบใดที่คลาสพื้นฐานของคุณเป็นแบบนามธรรมหรืออินเทอร์เฟซชนิดที่รู้จักจะถูกกำหนดโดยอัตโนมัติแทนที่จะต้องตกแต่งด้วยแอตทริบิวต์ประเภทที่รู้จัก

ในกรณีของฉันเองฉันเลือกที่จะใช้คุณสมบัติ $ type เพื่อกำหนดประเภทในวัตถุ json ของฉันแทนที่จะพยายามตรวจสอบจากคุณสมบัติแม้ว่าคุณสามารถยืมจากโซลูชันอื่น ๆ ที่นี่เพื่อใช้การพิจารณาตามคุณสมบัติ

 public class JsonKnownTypeConverter : JsonConverter
{
    public IEnumerable<Type> KnownTypes { get; set; }

    public JsonKnownTypeConverter() : this(ReflectTypes())
    {

    }
    public JsonKnownTypeConverter(IEnumerable<Type> knownTypes)
    {
        KnownTypes = knownTypes;
    }

    protected object Create(Type objectType, JObject jObject)
    {
        if (jObject["$type"] != null)
        {
            string typeName = jObject["$type"].ToString();
            return Activator.CreateInstance(KnownTypes.First(x => typeName == x.Name));
        }
        else
        {
            return Activator.CreateInstance(objectType);
        }
        throw new InvalidOperationException("No supported type");
    }

    public override bool CanConvert(Type objectType)
    {
        if (KnownTypes == null)
            return false;

        return (objectType.IsInterface || objectType.IsAbstract) && KnownTypes.Any(objectType.IsAssignableFrom);
    }

    public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
    {
        // Load JObject from stream
        JObject jObject = JObject.Load(reader);

        // Create target object based on JObject
        var target = Create(objectType, jObject);
        // Populate the object properties
        serializer.Populate(jObject.CreateReader(), target);
        return target;
    }

    public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
    {
        throw new NotImplementedException();
    }

    //Static helpers
    static Assembly CallingAssembly = Assembly.GetCallingAssembly();

    static Type[] ReflectTypes()
    {
        List<Type> types = new List<Type>();
        var referencedAssemblies = Assembly.GetExecutingAssembly().GetReferencedAssemblies();
        foreach (var assemblyName in referencedAssemblies)
        {
            Assembly assembly = Assembly.Load(assemblyName);
            Type[] typesInReferencedAssembly = GetTypes(assembly);
            types.AddRange(typesInReferencedAssembly);
        }

        return types.ToArray();
    }

    static Type[] GetTypes(Assembly assembly, bool publicOnly = true)
    {
        Type[] allTypes = assembly.GetTypes();

        List<Type> types = new List<Type>();

        foreach (Type type in allTypes)
        {
            if (type.IsEnum == false &&
               type.IsInterface == false &&
               type.IsGenericTypeDefinition == false)
            {
                if (publicOnly == true && type.IsPublic == false)
                {
                    if (type.IsNested == false)
                    {
                        continue;
                    }
                    if (type.IsNestedPrivate == true)
                    {
                        continue;
                    }
                }
                types.Add(type);
            }
        }
        return types.ToArray();
    }

สามารถติดตั้งเป็นฟอร์แมตเตอร์

GlobalConfiguration.Configuration.Formatters.JsonFormatter.SerializerSettings.Converters.Add(new JsonKnownTypeConverter());

1

ต่อไปนี้เป็นโซลูชันอื่นที่หลีกเลี่ยงการใช้งานjObject.CreateReader()และสร้างใหม่แทนJsonTextReader(ซึ่งเป็นพฤติกรรมที่ใช้โดยJsonCreate.Deserialzeวิธีการเริ่มต้น:

public abstract class JsonCreationConverter<T> : JsonConverter
{
    protected abstract T Create(Type objectType, JObject jObject);

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

    public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
    {
        if (reader.TokenType == JsonToken.Null)
            return null;

        // Load JObject from stream
        JObject jObject = JObject.Load(reader);

        // Create target object based on JObject
        T target = Create(objectType, jObject);

        // Populate the object properties
        StringWriter writer = new StringWriter();
        serializer.Serialize(writer, jObject);
        using (JsonTextReader newReader = new JsonTextReader(new StringReader(writer.ToString())))
        { 
            newReader.Culture = reader.Culture;
            newReader.DateParseHandling = reader.DateParseHandling;
            newReader.DateTimeZoneHandling = reader.DateTimeZoneHandling;
            newReader.FloatParseHandling = reader.FloatParseHandling;
            serializer.Populate(newReader, target);
        }

        return target;
    }

    public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
    {
        serializer.Serialize(writer, value);
    }
}

1

บ่อยครั้งที่การใช้งานจะมีอยู่ในเนมสเปซเดียวกันกับอินเตอร์เฟส ดังนั้นฉันมากับสิ่งนี้:

    public class InterfaceConverter : JsonConverter
    {
    public override bool CanWrite => false;
    public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
    {
    }

    public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
    {
        var token = JToken.ReadFrom(reader);
        var typeVariable = this.GetTypeVariable(token);
        if (TypeExtensions.TryParse(typeVariable, out var implimentation))
        { }
        else if (!typeof(IEnumerable).IsAssignableFrom(objectType))
        {
            implimentation = this.GetImplimentedType(objectType);
        }
        else
        {
            var genericArgumentTypes = objectType.GetGenericArguments();
            var innerType = genericArgumentTypes.FirstOrDefault();
            if (innerType == null)
            {
                implimentation = typeof(IEnumerable);
            }
            else
            {
                Type genericType = null;
                if (token.HasAny())
                {
                    var firstItem = token[0];
                    var genericTypeVariable = this.GetTypeVariable(firstItem);
                    TypeExtensions.TryParse(genericTypeVariable, out genericType);
                }

                genericType = genericType ?? this.GetImplimentedType(innerType);
                implimentation = typeof(IEnumerable<>);
                implimentation = implimentation.MakeGenericType(genericType);
            }
        }

        return JsonConvert.DeserializeObject(token.ToString(), implimentation);
    }

    public override bool CanConvert(Type objectType)
    {
        return !typeof(IEnumerable).IsAssignableFrom(objectType) && objectType.IsInterface || typeof(IEnumerable).IsAssignableFrom(objectType) && objectType.GetGenericArguments().Any(t => t.IsInterface);
    }

    protected Type GetImplimentedType(Type interfaceType)
    {
        if (!interfaceType.IsInterface)
        {
            return interfaceType;
        }

        var implimentationQualifiedName = interfaceType.AssemblyQualifiedName?.Replace(interfaceType.Name, interfaceType.Name.Substring(1));
        return implimentationQualifiedName == null ? interfaceType : Type.GetType(implimentationQualifiedName) ?? interfaceType;
    }

    protected string GetTypeVariable(JToken token)
    {
        if (!token.HasAny())
        {
            return null;
        }

        return token.Type != JTokenType.Object ? null : token.Value<string>("$type");
    }
}

ดังนั้นคุณสามารถรวมสิ่งนี้ทั่วโลกเช่น:

public static JsonSerializerSettings StandardSerializerSettings => new JsonSerializerSettings
    {
        Converters = new List<JsonConverter>
        {
            new InterfaceConverter()
        }
    };

1

การใช้แนวคิดของtotemและzlangnerฉันได้สร้างสิ่งKnownTypeConverterที่จะสามารถกำหนดผู้สืบทอดที่เหมาะสมที่สุดในขณะที่คำนึงถึงว่าข้อมูล json อาจไม่มีองค์ประกอบที่เป็นตัวเลือก

ดังนั้นบริการส่งการตอบสนอง JSON ที่มีอาร์เรย์ของเอกสาร (ขาเข้าและขาออก) เอกสารมีทั้งชุดองค์ประกอบทั่วไปและอีกชุด ในกรณีนี้องค์ประกอบที่เกี่ยวข้องกับเอกสารขาออกเป็นตัวเลือกและอาจขาด

ในเรื่องนี้คลาสพื้นฐานDocumentถูกสร้างขึ้นซึ่งมีชุดคุณสมบัติทั่วไป มีการสร้างคลาสผู้สืบทอดสองคลาสด้วย: - OutgoingDocumentเพิ่มองค์ประกอบเพิ่มเติมสองตัวเลือก"device_id"และ"msg_id"; - IncomingDocumentเพิ่มองค์ประกอบหนึ่งที่จำเป็น"sender_id";

ภารกิจคือการสร้างตัวแปลงที่ขึ้นอยู่กับข้อมูล json และข้อมูลจาก KnownTypeAttribute จะสามารถกำหนดคลาสที่เหมาะสมที่สุดที่ช่วยให้คุณสามารถบันทึกข้อมูลจำนวนมากที่สุดที่ได้รับ ควรพิจารณาว่าข้อมูล json อาจไม่มีองค์ประกอบเสริม เพื่อลดจำนวนการเปรียบเทียบองค์ประกอบ json และคุณสมบัติของตัวแบบข้อมูลฉันตัดสินใจไม่คำนึงถึงคุณสมบัติของคลาสพื้นฐานและสัมพันธ์กับองค์ประกอบ json เฉพาะคุณสมบัติของคลาสสืบทอด

ข้อมูลจากบริการ:

{
    "documents": [
        {
            "document_id": "76b7be75-f4dc-44cd-90d2-0d1959922852",
            "date": "2019-12-10 11:32:49",
            "processed_date": "2019-12-10 11:32:49",
            "sender_id": "9dedee17-e43a-47f1-910e-3a88ff6bc258",
        },
        {
            "document_id": "5044a9ac-0314-4e9a-9e0c-817531120753",
            "date": "2019-12-10 11:32:44",
            "processed_date": "2019-12-10 11:32:44",
        }
    ], 
    "total": 2
}

แบบจำลองข้อมูล:

/// <summary>
/// Service response model
/// </summary>
public class DocumentsRequestIdResponse
{
    [JsonProperty("documents")]
    public Document[] Documents { get; set; }

    [JsonProperty("total")]
    public int Total { get; set; }
}

// <summary>
/// Base document
/// </summary>
[JsonConverter(typeof(KnownTypeConverter))]
[KnownType(typeof(OutgoingDocument))]
[KnownType(typeof(IncomingDocument))]
public class Document
{
    [JsonProperty("document_id")]
    public Guid DocumentId { get; set; }

    [JsonProperty("date")]
    public DateTime Date { get; set; }

    [JsonProperty("processed_date")]
    public DateTime ProcessedDate { get; set; } 
}

/// <summary>
/// Outgoing document
/// </summary>
public class OutgoingDocument : Document
{
    // this property is optional and may not be present in the service's json response
    [JsonProperty("device_id")]
    public string DeviceId { get; set; }

    // this property is optional and may not be present in the service's json response
    [JsonProperty("msg_id")]
    public string MsgId { get; set; }
}

/// <summary>
/// Incoming document
/// </summary>
public class IncomingDocument : Document
{
    // this property is mandatory and is always populated by the service
    [JsonProperty("sender_sys_id")]
    public Guid SenderSysId { get; set; }
}

Converter:

public class KnownTypeConverter : JsonConverter
{
    public override bool CanConvert(Type objectType)
    {
        return System.Attribute.GetCustomAttributes(objectType).Any(v => v is KnownTypeAttribute);
    }

    public override bool CanWrite => false;

    public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
    {
        // load the object 
        JObject jObject = JObject.Load(reader);

        // take custom attributes on the type
        Attribute[] attrs = Attribute.GetCustomAttributes(objectType);

        Type mostSuitableType = null;
        int countOfMaxMatchingProperties = -1;

        // take the names of elements from json data
        HashSet<string> jObjectKeys = GetKeys(jObject);

        // take the properties of the parent class (in our case, from the Document class, which is specified in DocumentsRequestIdResponse)
        HashSet<string> objectTypeProps = objectType.GetProperties(BindingFlags.Instance | BindingFlags.Public)
            .Select(p => p.Name)
            .ToHashSet();

        // trying to find the right "KnownType"
        foreach (var attr in attrs.OfType<KnownTypeAttribute>())
        {
            Type knownType = attr.Type;
            if(!objectType.IsAssignableFrom(knownType))
                continue;

            // select properties of the inheritor, except properties from the parent class and properties with "ignore" attributes (in our case JsonIgnoreAttribute and XmlIgnoreAttribute)
            var notIgnoreProps = knownType.GetProperties(BindingFlags.Instance | BindingFlags.Public)
                .Where(p => !objectTypeProps.Contains(p.Name)
                            && p.CustomAttributes.All(a => a.AttributeType != typeof(JsonIgnoreAttribute) && a.AttributeType != typeof(System.Xml.Serialization.XmlIgnoreAttribute)));

            //  get serializable property names
            var jsonNameFields = notIgnoreProps.Select(prop =>
            {
                string jsonFieldName = null;
                CustomAttributeData jsonPropertyAttribute = prop.CustomAttributes.FirstOrDefault(a => a.AttributeType == typeof(JsonPropertyAttribute));
                if (jsonPropertyAttribute != null)
                {
                    // take the name of the json element from the attribute constructor
                    CustomAttributeTypedArgument argument = jsonPropertyAttribute.ConstructorArguments.FirstOrDefault();
                    if(argument != null && argument.ArgumentType == typeof(string) && !string.IsNullOrEmpty((string)argument.Value))
                        jsonFieldName = (string)argument.Value;
                }
                // otherwise, take the name of the property
                if (string.IsNullOrEmpty(jsonFieldName))
                {
                    jsonFieldName = prop.Name;
                }

                return jsonFieldName;
            });


            HashSet<string> jKnownTypeKeys = new HashSet<string>(jsonNameFields);

            // by intersecting the sets of names we determine the most suitable inheritor
            int count = jObjectKeys.Intersect(jKnownTypeKeys).Count();

            if (count == jKnownTypeKeys.Count)
            {
                mostSuitableType = knownType;
                break;
            }

            if (count > countOfMaxMatchingProperties)
            {
                countOfMaxMatchingProperties = count;
                mostSuitableType = knownType;
            }
        }

        if (mostSuitableType != null)
        {
            object target = Activator.CreateInstance(mostSuitableType);
            using (JsonReader jObjectReader = CopyReaderForObject(reader, jObject))
            {
                serializer.Populate(jObjectReader, target);
            }
            return target;
        }

        throw new SerializationException($"Could not serialize to KnownTypes and assign to base class {objectType} reference");
    }

    public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
    {
        throw new NotImplementedException();
    }

    private HashSet<string> GetKeys(JObject obj)
    {
        return new HashSet<string>(((IEnumerable<KeyValuePair<string, JToken>>) obj).Select(k => k.Key));
    }

    public static JsonReader CopyReaderForObject(JsonReader reader, JObject jObject)
    {
        JsonReader jObjectReader = jObject.CreateReader();
        jObjectReader.Culture = reader.Culture;
        jObjectReader.DateFormatString = reader.DateFormatString;
        jObjectReader.DateParseHandling = reader.DateParseHandling;
        jObjectReader.DateTimeZoneHandling = reader.DateTimeZoneHandling;
        jObjectReader.FloatParseHandling = reader.FloatParseHandling;
        jObjectReader.MaxDepth = reader.MaxDepth;
        jObjectReader.SupportMultipleContent = reader.SupportMultipleContent;
        return jObjectReader;
    }
}

PS: ในกรณีของฉันถ้าไม่มีผู้สืบทอดไม่ได้ถูกเลือกโดยตัวแปลง (สิ่งนี้สามารถเกิดขึ้นได้หากข้อมูล JSON มีข้อมูลจากคลาสฐานเท่านั้นหรือข้อมูล JSON ไม่มีองค์ประกอบที่เป็นตัวเลือกจากOutgoingDocument) ดังนั้นวัตถุของOutgoingDocumentคลาส จะถูกสร้างขึ้นเนื่องจากมีการระบุไว้เป็นอันดับแรกในรายการKnownTypeAttributeแอตทริบิวต์ ตามคำขอของคุณคุณสามารถเปลี่ยนแปลงการใช้งานKnownTypeConverterในสถานการณ์นี้ได้

โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.