Question
I'm developing an apartment booking service that aggregates data from multiple external apartment detail suppliers. Each supplier provides a response body containing property information, including a list of available rooms. My goal is to transform these diverse supplier responses into a unified 'Apartment Booking Response' format. Their are multiple Rooms available under one Property ID. But When I try to transform all the room related details to Apartment Search Details, Room related details are not mapping.
1. responseMapping.json:
Contains the JSON mappings to transform supplier response data.
{
"ResponseMapper": "{\"PROPERTY_ID\": [ \"properties[*].propertyId\", \"property_id\" ], \"NAME\": [ \"properties[*].propertyName\", \"rooms[*].room_name\", null ], \"ADDRESS\": [ null, null], \"STAR_RATING\": [ null, null], \"AMENITIES\": [ \"properties[*].rooms[*].benefits[*].benefitName\", \"rooms[*].rates[*].amenities[*].name\"], \"IMAGES\": [ null, null], \"LAT\": [ null, null], \"LON\": [ null, null], \"CANCELLATION_POLICY_FREE_CANCELLATION_UNTIL\": [ \"properties[*].rooms[*].cancellationPolicy.date[*].before\", \"rooms[*].rates[*].cancel_penalties[*].start\"], \"CANCELLATION_POLICY_TYPE\": [ \"properties[*].rooms[*].cancellationPolicy.code\", \"rooms[*].rates[*].refundable\" ], \"PRICE_NET_RATE\": [ \"properties[*].rooms[*].rate.exclusive\", \"rooms[*].rates[*].occupancy_pricing['1'].totals.exclusive.request_currency.value\" ], \"PRICE_FINAL_RATE\": [ \"properties[*].rooms[*].rate.inclusive\", \"rooms[*].rates[*].occupancy_pricing['1'].totals.inclusive.request_currency.value\"], \"PRICE_CURRENCY\": [ \"properties[*].rooms[*].rate.currency\", \"rooms[*].rates[*].occupancy_pricing['1'].totals.inclusive.request_currency.currency\" ], \"ROOM_ID\": [ \"properties[*].rooms[*].roomId\", \"rooms[*].id\" ], \"NO_OF_ADULTS\": [ \"properties[*].rooms[*].normalBedding\", \"rooms[*].no_of_adults\"], \"ROOM_PRICE_NET_PRICE\": [ \"properties[*].rooms[*].rate.exclusive\", \"rooms[*].rates[*].occupancy_pricing['1'].totals.exclusive.request_currency.value\"], \"ROOM_PRICE_GROSS_PRICE\": [ \"properties[*].rooms[*].rate.inclusive\", \"rooms[*].rates[*].occupancy_pricing['1'].totals.inclusive.request_currency.value\"], \"ROOM_PRICE_TOTAL_PRICE\": [ \"properties[*].rooms[*].totalPayment.inclusive\", \"rooms[*].rates[*].occupancy_pricing['1'].totals.property_inclusive.request_currency.value\"], \"ROOM_FREE_CANCELLATION_UNTIL\": [ \"properties[*].rooms[*].cancellationPolicy.date[*].before\", \"rooms[*].rates[*].cancel_penalties[*].start\"], \"ROOM_TYPE\": [ \"properties[*].rooms[*].cancellationPolicy.code\", \"rooms[*].rates[*].refundable\", ], \"ROOM_PAYMENT_TIMINGS\": [ \"properties[*].rooms[*].cancellationPolicy.parameter\", \"rooms[*].rates[*].cancel_penalties[*].parameter\"]}"
}
2. StringSupplierResponseTransformer.cs:
Implements the logic to transform supplier responses to the standardized DTO.
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using TransformMultipleResponse.Model;
public class SupplierResponseTransformer
{
private readonly JObject _responseMappings;
public SupplierResponseTransformer(string responseMappingsFile)
{
string mappingsJson = File.ReadAllText(responseMappingsFile).Trim();
JObject mappingsObject = JObject.Parse(mappingsJson);
string responseMapperString = mappingsObject["ResponseMapper"].ToString();
_responseMappings = JObject.Parse(responseMapperString);
}
public SupplierResponseTransformer(JObject responseMappings)
{
_responseMappings = responseMappings;
}
public async Task<List<ApartmentSearchResponseDTO>> TransformResponseAsync(string supplierResponseString)
{
JToken supplierData = JToken.Parse(supplierResponseString);
List<JToken> allSupplierEntries = new List<JToken>();
if (supplierData.Type == JTokenType.Array)
{
foreach (var item in supplierData)
{
if (item.Type == JTokenType.Array)
{
allSupplierEntries.AddRange(GetSupplierEntries(item));
}
else if (item.Type == JTokenType.Object)
{
allSupplierEntries.Add(item);
}
}
}
else if (supplierData.Type == JTokenType.Object)
{
allSupplierEntries.Add(supplierData);
}
else
{
throw new JsonReaderException("Unexpected JSON format");
}
var tasks = allSupplierEntries.Select(supplierEntry => Task.Run(() => TransformSupplierResponse((JObject)supplierEntry))).ToList();
var responses = await Task.WhenAll(tasks);
return responses.ToList();
}
private IEnumerable<JToken> GetSupplierEntries(JToken token)
{
if (token.Type == JTokenType.Array)
{
foreach (var item in token)
{
if (item.Type == JTokenType.Object)
{
yield return item;
}
else if (item.Type == JTokenType.Array)
{
foreach (var subItem in GetSupplierEntries(item))
{
yield return subItem;
}
}
}
}
else if (token.Type == JTokenType.Object)
{
yield return token;
}
}
//
private ApartmentSearchResponseDTO TransformSupplierResponse(JObject supplierEntry)
{
var propertyId = GetValue<int>(supplierEntry, "PROPERTY_ID");
var propertyName = GetValue<string>(supplierEntry, "NAME");
var address = GetValue<string>(supplierEntry, "ADDRESS");
var starRating = GetValue<int>(supplierEntry, "STAR_RATING");
var amenities = GetValue<List<string>>(supplierEntry, "AMENITIES");
var images = GetValue<List<string>>(supplierEntry, "IMAGES");
var lat = GetValue<double>(supplierEntry, "LAT");
var lon = GetValue<double>(supplierEntry, "LON");
var freeCancellationUntil = GetValue<string>(supplierEntry, "CANCELLATION_POLICY_FREE_CANCELLATION_UNTIL");
var cancellationType = GetValue<string>(supplierEntry, "CANCELLATION_POLICY_TYPE");
var netRate = GetValue<float>(supplierEntry, "PRICE_NET_RATE");
var finalRate = GetValue<float>(supplierEntry, "PRICE_FINAL_RATE");
var currency = GetValue<string>(supplierEntry, "PRICE_CURRENCY");
// Get all available room IDs
var rooms = new List<RoomDto>();
var roomIdTokens = new List<JToken>();
var roomIdPath = "";
// Find the first working path for room IDs
foreach (var path in GetPaths("ROOM_ID"))
{
if (string.IsNullOrEmpty(path))
continue;
try
{
var tokens = supplierEntry.SelectTokens(path).ToList();
if (tokens.Any())
{
roomIdTokens = tokens;
roomIdPath = path;
break;
}
}
catch
{
continue;
}
}
// Iterate through each room ID token
for (int i = 0; i < roomIdTokens.Count; i++)
{
var roomIdToken = roomIdTokens[i];
var roomId = roomIdToken.ToString();
// Create a room DTO with default values
var room = new RoomDto
{
RoomId = roomId,
NoOfAdults = 0,
Price = new RoomPriceDto
{
NetPrice = 0,
GrossPrice = 0,
TotalPrice = 0
},
Policies = new RoomPoliciesDto
{
Cancellation = new RoomCancellationDto
{
FreeCancellationUntil = null,
Type = null,
PaymentTimings = new List<string>()
}
}
};
// Helper function to set values safely
void SafeSetValue<T>(string placeholder, Action<T> setter)
{
foreach (var path in GetPaths(placeholder))
{
if (string.IsNullOrEmpty(path))
continue;
try
{
var tokens = supplierEntry.SelectTokens(path).ToList();
if (tokens.Count > i)
{
var token = tokens[i];
if (token != null)
{
if (typeof(T) == typeof(string))
{
setter((T)(object)token.ToString());
}
else if (typeof(T) == typeof(int) || typeof(T) == typeof(float) || typeof(T) == typeof(double))
{
setter((T)Convert.ChangeType(token, typeof(T)));
}
else if (typeof(T) == typeof(List<string>))
{
var list = new List<string>();
if (token.Type == JTokenType.Array)
{
foreach (var item in token)
{
list.Add(item.ToString());
}
}
else
{
list.Add(token.ToString());
}
setter((T)(object)list);
}
break;
}
}
}
catch
{
continue;
}
}
}
// Set room values using our helper
SafeSetValue<int>("NO_OF_ADULTS", value => room.NoOfAdults = value);
SafeSetValue<float>("ROOM_PRICE_NET_PRICE", value => room.Price.NetPrice = value);
SafeSetValue<float>("ROOM_PRICE_GROSS_PRICE", value => room.Price.GrossPrice = value);
SafeSetValue<float>("ROOM_PRICE_TOTAL_PRICE", value => room.Price.TotalPrice = value);
SafeSetValue<string>("ROOM_FREE_CANCELLATION_UNTIL", value => room.Policies.Cancellation.FreeCancellationUntil = value);
SafeSetValue<string>("ROOM_TYPE", value => room.Policies.Cancellation.Type = value);
SafeSetValue<List<string>>("ROOM_PAYMENT_TIMINGS", value => room.Policies.Cancellation.PaymentTimings = value);
rooms.Add(room);
}
var transformedResponse = new ApartmentSearchResponseDTO
{
Results = new List<ApartmentResultDTO>
{
new ApartmentResultDTO
{
PropertyId = propertyId,
Name = propertyName,
Address = address,
Location = new LocationDto
{
Lat = lat,
Lon = lon
},
StarRating = starRating,
Amenities = amenities,
Images = images,
CancellationPolicy = new CancellationPolicyDto
{
FreeCancellationUntil = freeCancellationUntil,
Type = cancellationType
},
Price = new PriceDto
{
NetRate = netRate,
FinalRate = finalRate,
Currency = currency
},
Rooms = rooms
}
},
Meta = null
};
return transformedResponse;
}
private T GetValue<T>(JToken source, string placeholder)
{
var paths = GetPaths(placeholder);
foreach (var path in paths)
{
try
{
if (string.IsNullOrEmpty(path))
continue;
var tokens = source.SelectTokens(path).ToList();
if (tokens.Count == 1)
{
var token = tokens.First();
if (typeof(T) == typeof(List<string>) && token.Type == JTokenType.Array)
{
return token.ToObject<T>();
}
return token.Value<T>();
}
else if (tokens.Count > 1)
{
if (typeof(T) == typeof(List<string>))
{
var list = tokens.Select(t => t.ToString()).ToList();
return (T)(object)list;
}
}
}
catch (Exception)
{
continue;
}
}
return default;
}
private List<string> GetPaths(string placeholder)
{
if (!_responseMappings.ContainsKey(placeholder))
{
return new List<string>();
}
JToken pathsToken = _responseMappings[placeholder];
if (pathsToken.Type == JTokenType.Array)
{
return pathsToken.ToObject<List<string>>();
}
else if (pathsToken.Type == JTokenType.String)
{
return new List<string> { pathsToken.ToString() };
}
return new List<string>();
}
}
3. Sample Response Body input
// Response 1:
string expediaResponseString = @"[
[
{
""property_id"": ""15375843"",
""status"": ""available"",
""rooms"": [
{
""id"": ""321209120"",
""room_name"": ""Property 1 Room (Beach Hideaway)"",
},
{
""id"": ""321209120"",
""room_name"": ""Property 1 Room (Beach Hideaway)"",
}
]
},
{
""property_id"": ""15375844"",
""status"": ""available"",
""rooms"": [
{
""id"": ""321209120"",
""room_name"": ""Property 2 Room (Beach Resort)"",
},
{
""id"": ""321209120"",
""room_name"": ""Property 2 Room (Beach Resort)"",
}
]
}
]
]"
// ------------------------------------------------------------------------------
// Response 2:
string agodaResponseString = @"[
{
""searchId"": 1642180748269790000,
""properties"": [
{
""propertyId"": 12159,
""propertyName"": ""Meeru Maldives Resort Island"",
""rooms"": [
{
""roomId"": 484877002,
""roomName"": ""Two Bedroom Villa"",
}, {
""roomId"": 484877003,
""roomName"": ""Three Bedroom Villa"",
},
]
},
{
""propertyId"": 12157,
""propertyName"": ""Medhufushi Island Resort"",
""rooms"": [
{
""roomId"": 484877004,
""roomName"": ""Three Bedroom Villa"",
}, {
""roomId"": 484877005,
""roomName"": ""Three Bedroom Villa"",
},
]
}
]
}
]";
4. Output of each Response body
Output which im getting if I pass expedia response body is correct.
4.1. Output if I pass Expedia Response body:
[
{
"Results": [
{
"PropertyId": 15375843,
"Name": "Room (Beach Hideaway)",
"Address": null,
"Location": {
"Lat": 0.0,
"Lon": 0.0
},
"StarRating": 0,
"Amenities": [
"Free WiFi",
"Free breakfast"
],
"Images": null,
"CancellationPolicy": {
"FreeCancellationUntil": "04/23/2025 17:30:00",
"Type": "True"
},
"Price": {
"NetRate": 2600.0,
"FinalRate": 3317.6,
"Currency": "USD"
},
"SupplierDetails": null,
"Rooms": [
{
"RoomId": "321209120",
"NoOfAdults": 0,
"Price": {
"NetPrice": 2600.0,
"GrossPrice": 3317.6,
"TotalPrice": 4629.6
},
"Policies": {
"Cancellation": {
"FreeCancellationUntil": "23-04-2025 17:30:00",
"Type": "True",
"PaymentTimings": []
}
}
},
{
"RoomId": "321209120",
"NoOfAdults": 0,
"Price": {
"NetPrice": 2600.0,
"GrossPrice": 3317.6,
"TotalPrice": 4629.6
},
"Policies": {
"Cancellation": {
"FreeCancellationUntil": "23-04-2025 17:30:00",
"Type": "True",
"PaymentTimings": []
}
}
}
]
}
],
"Meta": null
},
{
"Results": [
{
"PropertyId": 15375843,
"Name": "Room (Beach Hideaway)",
"Address": null,
"Location": {
"Lat": 0.0,
"Lon": 0.0
},
"StarRating": 0,
"Amenities": [
"Free WiFi",
"Free breakfast"
],
"Images": null,
"CancellationPolicy": {
"FreeCancellationUntil": "04/23/2025 17:30:00",
"Type": "True"
},
"Price": {
"NetRate": 2600.0,
"FinalRate": 3317.6,
"Currency": "USD"
},
"SupplierDetails": null,
"Rooms": [
{
"RoomId": "321209120",
"NoOfAdults": 0,
"Price": {
"NetPrice": 2600.0,
"GrossPrice": 3317.6,
"TotalPrice": 4629.6
},
"Policies": {
"Cancellation": {
"FreeCancellationUntil": "23-04-2025 17:30:00",
"Type": "True",
"PaymentTimings": []
}
}
}
]
}
],
"Meta": null
}
]
4.2. Output if i pass Agoda Response body:
[
{
"Results": [
{
"PropertyId": 12159,
"Name": "Meeru Maldives Resort Island",
"Address": null,
"Location": {
"Lat": 0.0,
"Lon": 0.0
},
"StarRating": 0,
"Amenities": [
"Breakfast",
"Dinner included",
"Buffet dinner",
],
"Images": null,
"CancellationPolicy": {
"FreeCancellationUntil": "03/22/2025 00:00:00",
"Type": "1D100P_100P"
},
"Price": {
"NetRate": 715.39,
"FinalRate": 912.84,
"Currency": "USD"
},
"SupplierDetails": null,
"Rooms": [
{
"RoomId": "972779372",
"NoOfAdults": 4,
"Price": {
"NetPrice": 1952.62,
"GrossPrice": 2491.54,
"TotalPrice": 2539.54
},
"Policies": {
"Cancellation": {
"FreeCancellationUntil": "22-03-2025 00:00:00",
"Type": "1D100P_100P",
"PaymentTimings": [
"{\r\n \"days\": 2,\r\n \"charge\": \"P\",\r\n \"value\": 0\r\n}",
"{\r\n \"days\": 1,\r\n \"charge\": \"P\",\r\n \"value\": 100\r\n}",
"{\r\n \"days\": 0,\r\n \"charge\": \"P\",\r\n \"value\": 100\r\n}"
]
}
}
}
]
}
],
"Meta": null
}
]
5. Issue in Response Body Structure
- Output which im getting while im passing Expedia Response body is correct, giving the proper expeceted result.
- If i pass Agods Response body is not giving the expected output, because its not giving me all the property's and all the rooms available in each respective property which is given in agodaResponse body.
- Although all the methods in SupplierResponseTransformer class should be Generic methods and should not hardcode the response body field paths in properties[*].rooms[] or "property_id" in any methods in SupplierResponseTransformer class.
- where if i pass Expedia Response body, it is giving all the property's and rooms available in the respective property in proper structure.
I want to adjust the methods in SupplierResponseTransformer class so it can able to transform any type of response body structure to 'Apartment Booking Response' format.