Search code examples
c#jsonserializationservicestackservicestack-text

ServiceStack JsonServiceClient Silently Failing When Deserializing


I am working on an integration with the Expedia API. I am relatively new to ServiceStack but have managed pretty well so far, however, I've hit an issue with the deserialization of a JSON response from the API which seems to be failing despite having JsConfig.ThrowOnDeserializationError enabled.

  JsConfig.Reset();
  JsConfig.ThrowOnDeserializationError = true;
  var availability = _client.Get<ExpediaHotelRoomAvailability>("avail?" + querystring);

In order to see the API response I am working with please use the following request:

GET http://dev.api.ean.com/ean-services/rs/hotel/v3/avail?minorRev=26&cid=55505&apiKey=cbrzfta369qwyrm9t5b8y8kf&customerUserAgent=Mozilla%2f5.0+(Windows+NT+6.3%3b+WOW64)+AppleWebKit%2f537.36+(KHTML%2c+like+Gecko)+Chrome%2f35.0.1916.114+Safari%2f537.36&customerIpAddress=%3a%3a1&hotelId=135857&arrivalDate=06%2f07%2f2014&departureDate=06%2f20%2f2014&includeDetails=true&includeRoomImages=true&room1=2 HTTP/1.1
User-Agent: Fiddler
Content-Type: text/json
Host: dev.api.ean.com

Here is a typical response:

{
   "HotelRoomAvailabilityResponse":{
      "@size":"1",
      "customerSessionId":"0ABAAA7A-D42E-2914-64D2-01C0F09040D8",
      "hotelId":135857,
      "arrivalDate":"06\/07\/2014",
      "departureDate":"06\/20\/2014",
      "hotelName":"La Quinta Inn and Suites Raleigh Cary",
      "hotelAddress":"191 Crescent Commons",
      "hotelCity":"Cary",
      "hotelStateProvince":"NC",
      "hotelCountry":"US",
      "numberOfRoomsRequested":1,
      "checkInInstructions":"",
      "tripAdvisorRating":4.5,
      "tripAdvisorReviewCount":189,
      "tripAdvisorRatingUrl":"http:\/\/www.tripadvisor.com\/img\/cdsi\/img2\/ratings\/traveler\/4.5-12345-4.gif",
      "HotelRoomResponse":{
         "rateCode":14587,
         "roomTypeCode":14587,
         "rateDescription":"Standard Room, 1 King Bed",
         "roomTypeDescription":"Standard Room, 1 King Bed",
         "supplierType":"E",
         "propertyId":67977,
         "BedTypes":{
            "@size":"1",
            "BedType":{
               "@id":"14",
               "description":"1 king"
            }
         },
         "smokingPreferences":"S,NS",
         "rateOccupancyPerRoom":3,
         "quotedOccupancy":2,
         "minGuestAge":0,
         "RateInfos":{
            "@size":"1",
            "RateInfo":{
               "@priceBreakdown":"true",
               "@promo":"false",
               "@rateChange":"true",
               "RoomGroup":{
                  "Room":{
                     "numberOfAdults":2,
                     "numberOfChildren":0,
                     "rateKey":"0214364d-6819-4631-aa97-a162c43e0297"
                  }
               },
               "ChargeableRateInfo":{
                  "@averageBaseRate":"109.61539",
                  "@averageRate":"109.61539",
                  "@commissionableUsdTotal":"1425.0",
                  "@currencyCode":"USD",
                  "@maxNightlyRate":"165.0",
                  "@nightlyRateTotal":"1425.0",
                  "@surchargeTotal":"229.19",
                  "@total":"1654.19",
                  "NightlyRatesPerRoom":{
                     "@size":"13",
                     "NightlyRate":[
                        {
                           "@baseRate":"90.0",
                           "@rate":"90.0",
                           "@promo":"false"
                        },
                        {
                           "@baseRate":"90.0",
                           "@rate":"90.0",
                           "@promo":"false"
                        },
                        {
                           "@baseRate":"100.0",
                           "@rate":"100.0",
                           "@promo":"false"
                        },
                        {
                           "@baseRate":"115.0",
                           "@rate":"115.0",
                           "@promo":"false"
                        },
                        {
                           "@baseRate":"115.0",
                           "@rate":"115.0",
                           "@promo":"false"
                        },
                        {
                           "@baseRate":"115.0",
                           "@rate":"115.0",
                           "@promo":"false"
                        },
                        {
                           "@baseRate":"165.0",
                           "@rate":"165.0",
                           "@promo":"false"
                        },
                        {
                           "@baseRate":"165.0",
                           "@rate":"165.0",
                           "@promo":"false"
                        },
                        {
                           "@baseRate":"90.0",
                           "@rate":"90.0",
                           "@promo":"false"
                        },
                        {
                           "@baseRate":"95.0",
                           "@rate":"95.0",
                           "@promo":"false"
                        },
                        {
                           "@baseRate":"95.0",
                           "@rate":"95.0",
                           "@promo":"false"
                        },
                        {
                           "@baseRate":"95.0",
                           "@rate":"95.0",
                           "@promo":"false"
                        },
                        {
                           "@baseRate":"95.0",
                           "@rate":"95.0",
                           "@promo":"false"
                        }
                     ]
                  },
                  "Surcharges":{
                     "@size":"1",
                     "Surcharge":{
                        "@type":"TaxAndServiceFee",
                        "@amount":"229.19"
                     }
                  }
               },
               "cancellationPolicy":"We understand that sometimes your travel plans change. We do not charge a change or cancel fee. However, this property (La Quinta Inn and Suites Raleigh Cary) imposes the following penalty to its customers that we are required to pass on: Cancellations or changes made after 6:00 PM ((GMT-05:00) Eastern Time (US &amp; Canada)) on Jun 6, 2014 are subject to a 1 Night Room &amp; Tax penalty. The property makes no refunds for no shows or early checkouts.",
               "CancelPolicyInfoList":{
                  "CancelPolicyInfo":[
                     {
                        "versionId":208699803,
                        "cancelTime":"18:00:00",
                        "startWindowHours":0,
                        "nightCount":1,
                        "currencyCode":"USD",
                        "timeZoneDescription":"(GMT-05:00) Eastern Time (US &amp; Canada)"
                     },
                     {
                        "versionId":208778550,
                        "cancelTime":"18:00:00",
                        "startWindowHours":24,
                        "nightCount":0,
                        "currencyCode":"USD",
                        "timeZoneDescription":"(GMT-05:00) Eastern Time (US &amp; Canada)"
                     }
                  ]
               },
               "nonRefundable":false,
               "rateType":"MerchantStandard",
               "currentAllotment":0,
               "guaranteeRequired":false,
               "depositRequired":true,
               "taxRate":229.19
            }
         },
         "ValueAdds":{
            "@size":"3",
            "ValueAdd":[
               {
                  "@id":"2048",
                  "description":"Free Wireless Internet"
               },
               {
                  "@id":"2",
                  "description":"Continental Breakfast"
               },
               {
                  "@id":"128",
                  "description":"Free Parking"
               }
            ]
         },
         "deepLink":"https:\/\/travel.ian.com\/templates\/55505\/hotels\/135857\/book?lang=en&amp;standardCheckin=06\/07\/2014&amp;standardCheckout=06\/20\/2014&amp;selectedPrice=1654.190000&amp;supplierType=E&amp;rateCode=14587&amp;roomTypeCode=14587&amp;roomsCount=1&amp;rooms[0].adultsCount=2&amp;rateKey=0214364d-6819-4631-aa97-a162c43e0297",
         "RoomImages":{
            "@size":"1",
            "RoomImage":{
               "url":"http:\/\/media.expedia.com\/hotels\/1000000\/70000\/68000\/67977\/67977_103_s.jpg"
            }
         }
      }
   }
}

A large part of the response is de-serialized fine except for all of the rate information which I am really interested in. No exceptions are thrown during the deserialization so I have little to go on in terms of tracking down the exact problem. To be more specific let's take the ChargeableRateInfo - all values contained within are either null or zero.

Here is my class I am trying to deserialize the response to:

namespace MyCustomNamespace
{
  using System.Collections.Generic;

  public class BedType
  {
    public string description { get; set; }
    public string id { get; set; }
  }

  public class BedTypes
  {
    public BedType BedType { get; set; }
    public int size { get; set; }
  }

  public class Room
  {
    public int numberOfAdults { get; set; }
    public int numberOfChildren { get; set; }
    public string rateKey { get; set; }
  }

  public class RoomGroup
  {
    public Room Room { get; set; }
  }

  public class NightlyRate
  {
    public string baseRate { get; set; }
    public string promo { get; set; }
    public string rate { get; set; }
  }

  public class NightlyRatesPerRoom
  {
    public List<NightlyRate> NightlyRate { get; set; }
    public int size { get; set; }
  }

  public class Surcharge
  {
    public decimal amount { get; set; }
    public string type { get; set; }
  }

  public class Surcharges
  {
    public Surcharge Surcharge { get; set; }
    public int size { get; set; }
  }

  public class ChargeableRateInfo
  {
    public NightlyRatesPerRoom NightlyRatesPerRoom { get; set; }
    public Surcharges Surcharges { get; set; }
    public decimal averageBaseRate { get; set; }
    public decimal averageRate { get; set; }
    public decimal commissionableUsdTotal { get; set; }
    public string currencyCode { get; set; }
    public decimal maxNightlyRate { get; set; }
    public decimal nightlyRateTotal { get; set; }
    public decimal surchargeTotal { get; set; }
    public decimal total { get; set; }
  }

  public class CancelPolicyInfo
  {
    public string cancelTime { get; set; }
    public string currencyCode { get; set; }
    public int nightCount { get; set; }
    public int percent { get; set; }
    public int startWindowHours { get; set; }
    public string timeZoneDescription { get; set; }
    public int versionId { get; set; }
  }

  public class CancelPolicyInfoList
  {
    public List<CancelPolicyInfo> CancelPolicyInfo { get; set; }
  }

  public class RateInfo
  {
    public CancelPolicyInfoList CancelPolicyInfoList { get; set; }
    public ChargeableRateInfo ChargeableRateInfo { get; set; }
    public RoomGroup RoomGroup { get; set; }
    public string cancellationPolicy { get; set; }
    public int currentAllotment { get; set; }
    public bool depositRequired { get; set; }
    public bool guaranteeRequired { get; set; }
    public bool nonRefundable { get; set; }
    public string priceBreakdown { get; set; }
    public string promo { get; set; }
    public string promoType { get; set; }
    public string rateChange { get; set; }
    public string rateType { get; set; }
    public decimal taxRate { get; set; }
  }

  public class RateInfos
  {
    public RateInfo RateInfo { get; set; }
    public int size { get; set; }
  }

  public class ValueAdd
  {
    public string description { get; set; }
    public string id { get; set; }
  }

  public class ValueAdds
  {
    public List<ValueAdd> ValueAdd { get; set; }
    public int size { get; set; }
  }

  public class RoomImage
  {
    public string url { get; set; }
  }

  public class RoomImages
  {
    public RoomImage RoomImage { get; set; }
    public int size { get; set; }
  }

  public class HotelRoomResponse
  {
    public BedTypes BedTypes { get; set; }
    public RateInfos RateInfos { get; set; }
    public RoomImages RoomImages { get; set; }
    public ValueAdds ValueAdds { get; set; }
    public string deepLink { get; set; }
    public int minGuestAge { get; set; }
    public int propertyId { get; set; }
    public int quotedOccupancy { get; set; }
    public int rateCode { get; set; }
    public string rateDescription { get; set; }
    public int rateOccupancyPerRoom { get; set; }
    public int roomTypeCode { get; set; }
    public string roomTypeDescription { get; set; }
    public string smokingPreferences { get; set; }
    public string supplierType { get; set; }
  }

  public class HotelRoomAvailabilityResponse
  {
    public List<HotelRoomResponse> HotelRoomResponse { get; set; }
    public string arrivalDate { get; set; }
    public string checkInInstructions { get; set; }
    public string customerSessionId { get; set; }
    public string departureDate { get; set; }
    public string hotelAddress { get; set; }
    public string hotelCity { get; set; }
    public string hotelCountry { get; set; }
    public int hotelId { get; set; }
    public string hotelName { get; set; }
    public string hotelStateProvince { get; set; }
    public int numberOfRoomsRequested { get; set; }
    public int size { get; set; }
    public decimal tripAdvisorRating { get; set; }
    public string tripAdvisorRatingUrl { get; set; }
    public int tripAdvisorReviewCount { get; set; }
  }

  public class ExpediaHotelRoomAvailability
  {
    public HotelRoomAvailabilityResponse HotelRoomAvailabilityResponse { get; set; }
  }
}

Could this possibly have something to do with the @ symbols in the JSON and if so how do I get around this?


Solution

  • You can handle the @ symbols named fields by providing their name using a [DataMember(Name=...)] attribute.

    Unfortunately this mean you have to provide a [DataContract] attribute on your DTO, which means all the properties then become opt-in. So all properties will need to have [DataMember] to be included, which makes the DTO less readable.

    So your NightlyRate DTO becomes:

    [DataContract]
    public class NightlyRate
    {
        [DataMember(Name="@baseRate")]
        public string baseRate { get; set; }
    
        [DataMember(Name="@promo")]
        public string promo { get; set; }
    
        [DataMember(Name="@rate")]
        public string rate { get; set; }
    }
    

    And NightlyRatesPerRoom would be:

    [DataContract]
    public class NightlyRatesPerRoom
    {
        [DataMember]
        public List<NightlyRate> NightlyRate { get; set; }
    
        [DataMember(Name="@size")]
        public int size { get; set; }
    }
    

    Obviously you will have to mark up the other DTOs as appropriate. I hope that helps.