Welcome to OGeek Q&A Community for programmer and developer-Open, Learning and Share
Welcome To Ask or Share your Answers For Others

Categories

0 votes
256 views
in Technique[技术] by (71.8m points)

c# - How to deserialize JSON with unnamed collection of types using DataContractSerializer

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.

See Question&Answers more detail:os

与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
Welcome To Ask or Share your Answers For Others

1 Reply

0 votes
by (71.8m points)

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.


与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
OGeek|极客中国-欢迎来到极客的世界,一个免费开放的程序员编程交流平台!开放,进步,分享!让技术改变生活,让极客改变未来! Welcome to OGeek Q&A Community for programmer and developer-Open, Learning and Share
Click Here to Ask a Question

...