DataContractJsonSerializer.ReadObject передают объект другого типа

У меня проблема, определенный http API может возвращать два разных типа объектов JSON. К сожалению, я должен жить с этим. Я должен работать с ним из кода.NET 3.5, и я использую DataContractJsonSerializer десериализовать ответы от сервиса. Это тоже ограничение - я не могу использовать что-либо еще для сериализации json. Когда я пытаюсь десериализовать объект типа 1 из объекта json типа 2 DataContractJsonSerializer просто успешно - только все свойства объекта установлены в значения по умолчанию. Есть ли способ заставить его потерпеть неудачу?

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)
    {
    }
}

1 ответ

Решение

У вас есть более основная проблема, чем создание serializer.ReadObject() вернуть ошибку: Stream вернулся из WebResponse.GetResponseStream() не может быть перемещен и прочитан со второго раза. Таким образом, в общем случае вам нужно будет скопировать ответ в некоторый локальный буфер и запросить то, что было возвращено. Есть как минимум два подхода к этому.

Во-первых, вы можете скопировать ответ в местный MemoryStream и попытаться десериализовать в ResponseDto, Затем, если это не удается, попробуйте SecondResponseDto, Чтобы различать два типа во время десериализации, вы можете пометить отличительные свойства с помощью [DataMember(IsRequired = true)],

Скажем например ResponseDto имеет члена data в то время как SecondResponseDto имеет члена results, Вы можете определить их следующим образом:

[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; }
}

А затем десериализовать следующим образом:

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;
    }
}

куда CopyTo() является методом расширения, адаптированным из этого ответа Nick:

public static class StreamExtensions
{
    // https://stackru.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);
        }
    }
}

(Этот метод расширения требуется только в.Net 3.5, так как.Net 4.0 и более поздние версии имеют Stream.CopyTo() встроенный.)

В этом решении отличительные элементы данных не обязательно должны присутствовать в контракте корневых данных. Пока [DataMember(IsRequired = true)] присутствует где-то в графе объектов, сериализатор сгенерирует исключение, если объект присутствует, а отмеченный элемент данных отсутствует.

Во-вторых, вы можете загрузить ответ в промежуточный XElement используя XmlReader вернулся JsonReaderWriterFactory.CreateJsonReader() и запрашивать возвращенные результаты, имея в виду отображение из JSON в XML, определенное в разделе Отображение между JSON и XML. Затем десериализуйте промежуточный XML в соответствующий тип в зависимости от присутствующих элементов. В приведенном выше случае ваш код может выглядеть так:

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());
}

Это решение использует тот факт, что DataContractJsonSerializer разделяет кодовую базу с DataContractSerializer и фактически работает путем внутреннего преобразования JSON в XML на лету во время десериализации. С этим решением больше не требуется отмечать отличительные элементы данных IsRequired = true,

Другие вопросы по тегам