How to deserialize JSON with unnamed collection of types using DataContractSerializer
Asked Answered
R

2

3

I'm using web service to get data about route mileage. Then I'm using deserializer to parse it out. Here is how JSON looks:

[{"__type":"CalculateMilesReport:http:\/\/pcmiler.alk.com\/APIs\/v1.0","RouteID":null,"TMiles":445.5]

With this response I had couple issues. Why is is wrapped into collection and how do I set object model? Also it was complaining about special __type attribute. So, I did "hack" and "prepped" string:

// Cut off first and last charachters [] - they send objects as arrays
rawJSON = rawJSON.Substring(1, rawJSON.Length - 2);

// Hide "__type" attribute as it messes up serializer with namespace
rawJSON = rawJSON.Replace("__type", "type");

Then everything worked with this object:

[DataContract]
public class PCMilerResponse
{
    [DataMember(Name = "Errors", EmitDefaultValue = false)]
    public PCMilerError[] Errors { get; set; }

    [DataMember(Name = "TMiles", EmitDefaultValue = false)]
    public decimal DrivingDistance { get; set; }    
}

Now I modified call to web service and I get following response

[
{"__type":"CalculateMilesReport:http:\/\/pcmiler.alk.com\/APIs\/v1.0","RouteID":null,"TMiles":445.5},
{"__type":"GeoTunnelReport:http:\/\/pcmiler.alk.com\/APIs\/v1.0","RouteID":null,"GeoTunnelPoints":
    [{"Lat":"34.730466","Lon":"-92.247147"},{"Lat":"34.704863","Lon":"-92.29329"},{"Lat":"34.676312","Lon":"-92.364654"},{"Lat":"29.664271","Lon":"-95.236735"}]
}
]

Now it makes sense why there is array and "__type". But I'm not sure how to write object to properly parse it. I guess special attributes need to be applied and maybe generic array need to be there? Any help on how to properly deserialize it?

P.S. I can do more hacking and replace those strings making it object with 2 objects inside, but I wonder if there is "proper" way to handle it.

Roup answered 20/12, 2015 at 19:18 Comment(7)
The structure of the JSON doesn't really seem to match your class structure.Hiltner
If you look at how I "hack" json - then it does match. In this example JSON doesn't have any Errors, so property will be parsed as null. But TMiles is there and will be on PCMilerResponse after deserialize.Roup
Doesn't the DataContract attribute have any parameters?Bacteroid
btw at "objecst" I guess you meant to write "objects"Bacteroid
I don't know why my finders type like this :)Roup
Does the web service have any documentation explaining the types that might be returned?Takeshi
Or does the web service provide a WSDL?Takeshi
T
5

The "__type" parameter is added by DataContractJsonSerializer to represent polymorphic type information. From the docs:

Polymorphism

Polymorphic serialization consists of the ability to serialize a derived type where its base type is expected. This is supported for JSON serialization by WCF comparable to the way XML serialization is supported. For example, you can serialize MyDerivedType where MyBaseType is expected, or serialize Int where Object is expected...

Preserving Type Information

As stated earlier, polymorphism is supported in JSON with some limitations...

To preserve type identity, when serializing complex types to JSON a "type hint" can be added, and the deserializer recognizes the hint and acts appropriately. The "type hint" is a JSON key/value pair with the key name of "__type" (two underscores followed by the word "type"). The value is a JSON string of the form "DataContractName:DataContractNamespace" (anything up to the first colon is the name).

In order to use this mechanism to (de)serialize a polymorphic type, all possible derived types must be specified up front to DataContractJsonSerializer. See Data Contract Known Types for a discussion of how to do this.

Thus, it looks like your web service is returning an array of polymorphic types. How to handle this?

The Manual Solution

One possible solution to your problem is to manually create a c# class hierarchy corresponding to the data contact hierarchy, properly annotated with DataContract and DataMember attributes. Then you can leverage the "type hint" functionality of the data contract serializers to cause the correct subclass to be created automatically during deserialization. Courtesy of google, the classes you are seeing look to be documented at PC*MILER Web Services API: Report Class. Using this documentation, your classes should look like:

public static class Namespaces
{
    public const string Pcmiler = @"http://pcmiler.alk.com/APIs/v1.0";
}

[DataContract(Namespace = Namespaces.Pcmiler)]
public class Coordinates
{
    public double Lat { get; set; }
    public double Lon { get; set; }
}

[KnownType(typeof(CalculateMilesReport))]
[KnownType(typeof(GeoTunnelReport))]
[DataContract(Namespace = Namespaces.Pcmiler)]
public abstract class Report
{
    [DataMember]
    public string RouteID { get; set; }
}

[DataContract(Namespace = Namespaces.Pcmiler)]
public class CalculateMilesReport : Report
{
    [DataMember]
    public double TMiles { get; set; }
}

[DataContract(Namespace = Namespaces.Pcmiler)]
public class GeoTunnelReport : Report
{
    [DataMember]
    public List<Coordinates> GeoTunnelPoints { get; set; }
}

Note the [KnownType(typeof(XXXReport))] attributes attached to Report. In order to deserialize the JSON correctly, all expected subclasses of Report must appear as known types. According to the documentation there are 11 possible subclasses, so you will need to provide classes for all of them that you might receive from your web service.

Now you can deserialize your rawJSON as a List<Report>, and everything in your sample JSON should read in correctly, because you have correctly matched the data contract names, namespaces, and type hierarchies to that of the web service:

        var list = DataContractJsonSerializerHelper.GetObject<List<Report>>(rawJSON);

using

public static class DataContractJsonSerializerHelper
{
    private static MemoryStream GenerateStreamFromString(string value)
    {
        return new MemoryStream(Encoding.Unicode.GetBytes(value ?? ""));
    }

    public static T GetObject<T>(string json)
    {
        var serializer = new DataContractJsonSerializer(typeof(T));
        using (var stream = GenerateStreamFromString(json))
        {
            return (T)serializer.ReadObject(stream);
        }
    }
}

However, that web service looks rather elaborate. Manually recreating all its classes would be tiresome.

The Automatic Solution

Since it appears your web service is a WCF service, hopefully they have published its Service Metadata. If they have, it will allow you to generate a client automatically using Add Service Reference in Visual Studio. For instructions on how to do this, see How to: Create a Windows Communication Foundation Client and How to: Add, Update, or Remove a Service Reference.

Again courtesy of google, it appears your service does provide its metadata, at http://pcmiler.alk.com/APIs/REST/v1.0/service.svc?wsdl. Doing

 svcutil.exe http://pcmiler.alk.com/APIs/REST/v1.0/service.svc?wsdl

Seems to generate a plausible set of client classes consistent with the manual classes created above. However, you should doublecheck the documentation from your web service to ensure this the correct way to consume their service metadata.

Once a client has been created, you can access the web service as if you were calling a local c# API. See Accessing Services Using a WCF Client for how. The article Creating and Consuming Your First WCF Service gives an overview of the entire process.

Takeshi answered 23/12, 2015 at 12:22 Comment(0)
W
1

About making the __type attribute disappear there are discussions on SO.

Here is one, which solved in the following way:

change the WebMethod return type to object, i.e.

[WebMethod]
public static object ApplyCredits(int addonid, int[] vehicleIds) 

instead of

[WebMethod]
public static WebMethodReturn ApplyCredits(int addonid, int[] veh

Another one solved by

Adding the namespace parameter [DataContract(Namespace = "")] to the data contract.

I'm not sure how to write object to properly parse it

Based on the response you can build classes in which your JSON is going to fit, but since you have the model classes you are supposed to use the same from which your JSON was built. Maybe I didn't get here something correctly from your question.

Here is a crafted model example in which your JSON would fit in:

public class ResultType
{
    public string RouteID { get; set; }
    public List<GeoTunnelPoints> Points { get; set; }
    public double TMiles { get; set; }

    public ResultType()
    {
        RouteID = "";
        Points = new List<GeoTunnelPoints>();
        TMiles = 0;
    }
}

public class GeoTunnelPoints
{
    double Lat { get; set; }
    double Lon { get; set; }

    public GeoTunnelPoints()
    {
        Lat = 0.0;
        Lon = 0.0;
    }
}

Example usage:

// Your example JSON after excluding the __type
string input = 
                "[" +
                    "{" + 
                       "\"RouteID\":null, " +
                        "\"TMiles\":445.5}," +
                    "{" +
                        "\"RouteID\":null," +
                        "\"GeoTunnelPoints\":" +
                              "[" +
                                  "{\"Lat\":\"34.730466\",\"Lon\":\"-92.247147\"}," +
                                  "{\"Lat\":\"34.704863\",\"Lon\":\"-92.29329\"}," +
                                  "{\"Lat\":\"34.676312\",\"Lon\":\"-92.364654\"}," +
                                  "{\"Lat\":\"29.664271\",\"Lon\":\"-95.236735\"}" +
                              "]" +
                    "} " +
                "]";

List<ResultType> resultList = new List<ResultType>();
// This will be your C# result collection
resultList = new JavaScriptSerializer().Deserialize<List<ResultType>>(input);
Whap answered 23/12, 2015 at 6:52 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.