2

I have a problem, a certain http API can return two different types of JSON objects. Unfortunately I have to live with it. I have to work with it from .NET 3.5 code, and I use DataContractJsonSerializer to deserialize the responses from the service. This is a constraint as well - I can't use anything else for json serialization. When I try to deserialize object of Type 1 from json object of Type 2 DataContractJsonSerializer just succeeds - only all the properties of the object are set to default values. Is there any way to make it fail?

ResponseDto Get<ResponseDto>(string requestUrl)
{
    // skip all the HttpWebRequest bullshit
    try
    {
       var response = request.GetResponse();
       if (response.StatusCode = HttpStatusCode.Ok)
       {
          var serializer = new DataContractJsonSerializer(typeof(ResponseDto));

          // I would like this line to fail somehow, give me null back, whatever
          var responseDto = (ResponseDto)serializer.ReadObject(response.GetResponseStream());

          // this never happens now
          if (responseDto == null)
          {
             var otherResponseSerializer = new DataContractJsonSerializer(typeof(SecondResponseDto));
            // SecondResponseDto is always fixed type
             var otherResponse = (SecondResponseDto)otherResponseSerializer.ReadObject(response.GetResponseStream());

             // this typically would throw an exception
             HandleOtherResponse(otherResponse);

             return default(ResponseDto);
          }
       } 
    }
    catch(WebException e)
    {
    }
}
dbc
  • 104,963
  • 20
  • 228
  • 340
Myar
  • 112
  • 10
  • How different are the responses? Can you share a simplified example? – dbc Jan 19 '18 at 23:37
  • You can set `[DataMember(IsRequired = true)]` on the distinct members of `ResponseDto` and `SecondResponseDto`, and an exception will be thrown if the member is not present. But as an aside, does getting and reading from the response stream **twice** actually work? That sort of surprises me. – dbc Jan 19 '18 at 23:44

1 Answers1

1

You have a more basic problem here than making serializer.ReadObject() return an error: the Stream returned from WebResponse.GetResponseStream() cannot be repositioned and read from a second time. Thus in general you will need to copy the response into some local buffer and query what was returned. There are at least two approaches to this.

Firstly, you could copy the response into a local MemoryStream and attempt to deserialize into ResponseDto. Then if that fails, try SecondResponseDto. To distinguish between the two types during deserialization, you can mark distinguishing properties with [DataMember(IsRequired = true)].

Say for instance ResponseDto has a member data while SecondResponseDto has a member results. You could define them as follows:

[DataContract]
public class ResponseDto
{
    [DataMember(Name = "data", IsRequired = true)]
    public Data data { get; set; }
}

[DataContract]
public class SecondResponseDto
{
    [DataMember(Name = "results", IsRequired = true)]
    public List<Result> Results { get; set; }
}

And then deserialize as follows:

ResponseDto response1;
SecondResponseDto response2;

var copyStream = new MemoryStream();
using (var response = (HttpWebResponse)request.GetResponse())
{
    if (response.StatusCode == HttpStatusCode.OK)
    {
        using (var responseStream = response.GetResponseStream())
        {
            responseStream.CopyTo(copyStream);
        }
    }
}

try
{
    var serializer = new DataContractJsonSerializer(typeof(ResponseDto));
    copyStream.Position = 0L;
    response1 = (ResponseDto)serializer.ReadObject(copyStream);
}
catch (SerializationException)
{
    response1 = null;
}

if (response1 != null)
    response2 = null;
else
{
    try
    {
        var otherResponseSerializer = new DataContractJsonSerializer(typeof(SecondResponseDto));
        copyStream.Position = 0L;
        response2 = (SecondResponseDto)otherResponseSerializer.ReadObject(copyStream);
    }
    catch (SerializationException)
    {
        response2 = null;
    }
}

Where CopyTo() is an extension method adapted from this answer by Nick:

public static class StreamExtensions
{
    // https://stackoverflow.com/questions/230128/how-do-i-copy-the-contents-of-one-stream-to-another
    public static void CopyTo(this Stream input, Stream output)
    {
        byte[] buffer = new byte[32768];
        int read;
        while ((read = input.Read(buffer, 0, buffer.Length)) > 0)
        {
            output.Write(buffer, 0, read);
        }
    }
}

(This extension method is only required in .Net 3.5 as .Net 4.0 and later have Stream.CopyTo() built in.)

The distinguishing data member(s) do not need to be present on the root data contract in this solution. As long as [DataMember(IsRequired = true)] is present somewhere in the object graph the serializer will throw an exception if the object is present but the marked data member is not.

Secondly, you could load the response into an intermediate XElement using an XmlReader returned by JsonReaderWriterFactory.CreateJsonReader() and query the returned results keeping in mind the mapping from JSON to XML defined in Mapping Between JSON and XML. Then deserialize the intermediate XML to the appropriate type depending on the elements present. In the case above your code might look like:

ResponseDto response1 = null;
SecondResponseDto response2 = null;

XElement root = null;

using (var response = (HttpWebResponse)request.GetResponse())
{
    if (response.StatusCode == HttpStatusCode.OK)
    {
        using (var responseStream = response.GetResponseStream())
        using (var reader = JsonReaderWriterFactory.CreateJsonReader(responseStream, XmlDictionaryReaderQuotas.Max))
        {
            root = XElement.Load(reader);
        }
    }
}

// Replace the Where queries below with something appropriate to your actual JSON.

if (root != null && root.Elements().Where(e => e.Name.LocalName == "data").Any())
{
    var serializer = new DataContractJsonSerializer(typeof(ResponseDto));
    response1 = (ResponseDto)serializer.ReadObject(root.CreateReader());
}
else if (root != null && root.Elements().Where(e => e.Name.LocalName == "results").Any())
{
    var serializer = new DataContractJsonSerializer(typeof(SecondResponseDto));
    response2 = (SecondResponseDto)serializer.ReadObject(root.CreateReader());
}

This solution takes advantage of the fact that DataContractJsonSerializer shares a code base with DataContractSerializer and actually works by internally translating the JSON to XML on the fly during deserialization. With this solution it is no longer required to mark distinguishing data members with IsRequired = true.

dbc
  • 104,963
  • 20
  • 228
  • 340
  • I know about the Stream repositioning, that wasn't the part of the question since I know how to solve it – Myar Jan 20 '18 at 10:03