/// <inheritdoc /> /// <remarks> /// - Methodology /// - Is the type Two-Way bindable and has the type of the object written into the json? If yes then strip /// out the type information and ask Json.net to deserialize into that type. If not then... /// - Get all child types of the specified type. /// - Filter to child types where every 1st level JSON property is either a public (accessor is public) /// property or a public field of the child type, using case-insensitive matching. Child types passing /// this filter are called 'candidates'. /// - If there are no candidates, then throw. /// - For all candidates, ask the serializer to deserialize the JSON as the candidate type. Catch and /// ignore exceptions when attempting to deserialize. If only one candidate successfully deserializes /// then return the deserialized object. /// - If more than one candidate successfully deserializes, then filter to candidates whose public /// properites and fields are all 1st level JSON properties, using case-insensitive matching. We call /// this "strict matching." If only one candidate has a strict match, return the corresponding /// deserialized object. Otherwise, throw. /// - Using the serializer to deserialize enables the method to support types with constructors, /// which JSON.net does well out-of-the-box and which would be cumbersome to emulate. /// - This method does not consider > 1st level JSON properties to determine candidates. In other words, /// it is not matching on the fields/properties of the child's fields/properties (e.g. the match is done /// on Dog.Owner, not Dog.Owner.OwnersAddress). The issue is that that kind of matching would require /// complex logic. For example, Strings have properties such as Length which would need to be ignored /// when reflecting. Similarly, all fields/properties of value-types would need to be ignored. There /// are likely other corner-cases. To keep things simple, if the 1st level properties match the type's /// properties and fields, by name, we hand-off the problem to the serializer and let it throw if /// there is some incompatability deep in the field/property hierarchy. /// - This method does not consider the type of objects containted within JSON arrays to determine /// candidates. This is difficult because JSON arrays can contain a mix of types (just likes .net /// objects) and every element would have to be deserialized and matched against the child's IEnumerable /// type and undoubtedly complexity would arise from dealing with generics and the vast implementations /// of IEnumerable. Like the bullet above, we simply hand-off the problem to the serializer. /// - If properties or fields are removed from a child type after it has been serialized, then the JSON /// will not deserialize properly because that child type will no longer be a candidate. If, however, /// properties or fields are added to the child type, then the child type will continue to be a /// candidate for the serialized JSON. /// - If the user serializes private or internal fields/properties, then this method will not work because /// it only looks for public fields/properties. We cannot bank on the JSON having been serialized by /// the same serializer passed to this method. Even if we could, the serializer is so highly /// configurable that it would be difficult to determine whether or which internal or private fields /// or properties are serialized. /// - It's OK if the JSON is serialized with NullValueHandling.Ignore because the candidate filter tries /// to find all JSON properties in child type's properties/fields, and not vice-versa. However, depending /// on how permissive the serializer's Contract Resolver is, those candidates may or may not be able to /// be deserialized. For example, if constructor parameters are required and a particular parameter is /// excluded from the JSON, then that type cannot be deserialized. /// </remarks> public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) { if (reader == null) { throw new ArgumentNullException("reader"); } if (serializer == null) { throw new ArgumentNullException("serializer"); } if (reader.TokenType == JsonToken.Null) { return(null); } var jsonObject = JObject.Load(reader); var jsonProperties = new HashSet <string>(GetProperties(jsonObject), StringComparer.OrdinalIgnoreCase); // if two-way bindable then then type should be written into the json // if it's not then fallback on the typical strategy var bindableAttribute = GetBindableAttribute(objectType); if (IsTwoWayBindable(bindableAttribute) && jsonProperties.Contains(TypeTokenName)) { return(ReadUsingTypeSpecifiedInJson(serializer, jsonObject)); } var childTypes = GetChildTypes(objectType); var candidateChildTypes = GetCandidateChildTypes(childTypes, jsonProperties); var deserializedChildren = DeserializeCandidates(candidateChildTypes, serializer, jsonObject).ToList(); if (deserializedChildren.Count == 0) { throw new JsonSerializationException( string.Format(CultureInfo.InvariantCulture, "Unable to deserialize to type {0}, value: {1}", objectType, jsonObject)); } CandidateChildType matchedChild = null; if (deserializedChildren.Count == 1) { matchedChild = deserializedChildren.Single(); } else if (deserializedChildren.Count > 1) { matchedChild = SelectBestChildUsingStrictPropertyMatching(deserializedChildren, jsonObject, jsonProperties); } return(matchedChild.DeserializedObject); }
private static IEnumerable <CandidateChildType> GetCandidateChildTypes(IEnumerable <Type> childTypes, HashSet <string> jsonProperties) { var candidateChildTypes = new List <CandidateChildType>(); foreach (var childType in childTypes) { var childTypeProperties = childType.GetProperties().Select(t => t.Name).ToList(); var childTypeFields = childType.GetFields().Select(t => t.Name).ToList(); var childTypePropertiesAndFields = new HashSet <string>( childTypeProperties.Concat(childTypeFields), StringComparer.OrdinalIgnoreCase); if (jsonProperties.All(p => childTypePropertiesAndFields.Contains(p))) { var candidateChildType = new CandidateChildType { Type = childType, PropertiesAndFields = childTypePropertiesAndFields }; candidateChildTypes.Add(candidateChildType); } } return(candidateChildTypes); }