Reputation: 556
I have a third-party API that is returning a JSON object with an array with varying types. For example, one array json object looks something like this:
[{
"text": "<p>Introduction</p>",
"id": 13273,
"item_type": "Message",
"page_id": 5292,
"position": 1,
"alias": null,
"html_class": null,
"include_condition": null
}, {
"text": "<p><span style=\"background-color: transparent; color: rgb(0, 0, 0);\">Value Proposition</span></p>",
"id": 13274,
"item_type": "Message",
"page_id": 5292,
"position": 2,
"alias": null,
"html_class": null,
"include_condition": null
}, {
"start_value": 0,
"end_value": 10,
"start_text": "Not At All Common Need",
"mid_text": null,
"end_text": "Extremely Common Need",
"enable_not_applicable": false,
"not_applicable_text": null,
"choices": [{
"id": 43181,
"text": "0",
"alias": null,
"position": 1,
"is_default": false,
"is_other": false,
"is_none_of_above": false,
"points": 0.0000,
"image": null
}, {
"id": 43182,
"text": "1",
"alias": null,
"position": 2,
"is_default": false,
"is_other": false,
"is_none_of_above": false,
"points": 1.0000,
"image": null
}, {
"id": 43183,
"text": "2",
"alias": null,
"position": 3,
"is_default": false,
"is_other": false,
"is_none_of_above": false,
"points": 2.0000,
"image": null
}, {
"id": 43184,
"text": "3",
"alias": null,
"position": 4,
"is_default": false,
"is_other": false,
"is_none_of_above": false,
"points": 3.0000,
"image": null
}, {
"id": 43185,
"text": "4",
"alias": null,
"position": 5,
"is_default": false,
"is_other": false,
"is_none_of_above": false,
"points": 4.0000,
"image": null
}, {
"id": 43186,
"text": "5",
"alias": null,
"position": 6,
"is_default": false,
"is_other": false,
"is_none_of_above": false,
"points": 5.0000,
"image": null
}, {
"id": 43187,
"text": "6",
"alias": null,
"position": 7,
"is_default": false,
"is_other": false,
"is_none_of_above": false,
"points": 6.0000,
"image": null
}, {
"id": 43188,
"text": "7",
"alias": null,
"position": 8,
"is_default": false,
"is_other": false,
"is_none_of_above": false,
"points": 7.0000,
"image": null
}, {
"id": 43189,
"text": "8",
"alias": null,
"position": 9,
"is_default": false,
"is_other": false,
"is_none_of_above": false,
"points": 8.0000,
"image": null
}, {
"id": 43190,
"text": "9",
"alias": null,
"position": 10,
"is_default": false,
"is_other": false,
"is_none_of_above": false,
"points": 9.0000,
"image": null
}, {
"id": 43191,
"text": "10",
"alias": null,
"position": 11,
"is_default": false,
"is_other": false,
"is_none_of_above": false,
"points": 10.0000,
"image": null
}],
"layout": "Horizontal",
"show_separator": false,
"option_width": null,
"question_text": "<p><span style=\"background-color: transparent; color: rgb(0, 0, 0);\">To what degree is the need described common in your research/work? (choose only one)</span></p>",
"subtext": "",
"is_required": true,
"item_position": "Left",
"question_text_position": "Top",
"id": 13275,
"item_type": "RadioButtonScale",
"page_id": 5292,
"position": 3,
"alias": "",
"html_class": null,
"include_condition": null
}, {
"script": "\n$(function() {\n localStorage.setItem('Speeder', new Date());\n $('.btn-next').hide();\n $('.btn-prev').hide();\n\n function BIO_showButton() {\n $('.btn-next').show();\n $('.btn-prev').show();\n }\n setTimeout(BIO_showButton, 5000);\n namespace.checkForSurveyComplete();\n});",
"id": 13276,
"item_type": "Javascript",
"page_id": 5292,
"position": 4,
"alias": null,
"html_class": null,
"include_condition": null
}]
The other array looks like this:
{{
"rows": [
{
"id": 1123,
"position": 1,
"row_type": "Normal",
"text": "First Row",
"alias": null,
"include_condition": {
"expressions": [],
"groups": [],
"logical_operator": "OR"
}
},
{
"id": 1124,
"position": 2,
"row_type": "Normal",
"text": "Second Row",
"alias": null,
"include_condition": {
"expressions": [],
"groups": [],
"logical_operator": "OR"
}
},
{
"id": 1125,
"position": 3,
"row_type": "Normal",
"text": "Thrid Row",
"alias": null,
"include_condition": {
"expressions": [],
"groups": [],
"logical_operator": "OR"
}
},
{
"id": 1126,
"position": 4,
"row_type": "Normal",
"text": "Fourth Row",
"alias": null,
"include_condition": {
"expressions": [],
"groups": [],
"logical_operator": "OR"
}
}
],
"columns": [
{
"id": 1079,
"position": 1,
"column_type": "RowTexts",
"prototype_item": null,
"require_unique_answers": false,
"width": 0
},
{
"id": 1080,
"position": 2,
"column_type": "Question",
"prototype_item": {
"layout": "Vertical",
"columns": 1,
"show_number_labels": false,
"choices": [
{
"id": 43713,
"text": "1st Radio Grid",
"alias": null,
"position": 0,
"is_default": false,
"is_other": false,
"is_none_of_above": false,
"points": null,
"image": null
},
{
"id": 43714,
"text": "2nd Radio Grid",
"alias": null,
"position": 1,
"is_default": false,
"is_other": false,
"is_none_of_above": false,
"points": null,
"image": null
},
{
"id": 43715,
"text": "3rd Radio Grid",
"alias": null,
"position": 2,
"is_default": false,
"is_other": false,
"is_none_of_above": false,
"points": null,
"image": null
},
{
"id": 43716,
"text": "4th Radio Grid",
"alias": null,
"position": 3,
"is_default": false,
"is_other": false,
"is_none_of_above": false,
"points": null,
"image": null
},
{
"id": 43717,
"text": "Last Radio Grid",
"alias": null,
"position": 4,
"is_default": false,
"is_other": false,
"is_none_of_above": false,
"points": null,
"image": null
}
],
"randomize": false,
"allow_other": false,
"question_text": "<p><span style=\"font-family: Lato;\">Select one of each</span></p>",
"subtext": "",
"is_required": false,
"item_position": "Left",
"question_text_position": "Top",
"id": 13516,
"item_type": "RadioButtons",
"alias": null,
"html_class": null,
"include_condition": null
},
"require_unique_answers": false,
"width": 0
}
],
"elements": [
{
"row": 1,
"column": 1,
"item": {
"text": "First Row",
"id": 13517,
"item_type": "Message",
"alias": null,
"html_class": null,
"include_condition": null
}
},
{
"row": 1,
"column": 2,
"item": {
"prototype_item_type": "RadioButtons",
"prototype_item_id": 13516,
"id": 13521,
"item_type": "MatrixQuestionColumnElement",
"alias": null,
"html_class": null,
"include_condition": null
}
},
{
"row": 2,
"column": 1,
"item": {
"text": "Second Row",
"id": 13518,
"item_type": "Message",
"alias": null,
"html_class": null,
"include_condition": null
}
},
{
"row": 2,
"column": 2,
"item": {
"prototype_item_type": "RadioButtons",
"prototype_item_id": 13516,
"id": 13522,
"item_type": "MatrixQuestionColumnElement",
"alias": null,
"html_class": null,
"include_condition": null
}
},
{
"row": 3,
"column": 1,
"item": {
"text": "Thrid Row",
"id": 13519,
"item_type": "Message",
"alias": null,
"html_class": null,
"include_condition": null
}
},
{
"row": 3,
"column": 2,
"item": {
"prototype_item_type": "RadioButtons",
"prototype_item_id": 13516,
"id": 13523,
"item_type": "MatrixQuestionColumnElement",
"alias": null,
"html_class": null,
"include_condition": null
}
},
{
"row": 4,
"column": 1,
"item": {
"text": "Fourth Row",
"id": 13520,
"item_type": "Message",
"alias": null,
"html_class": null,
"include_condition": null
}
},
{
"row": 4,
"column": 2,
"item": {
"prototype_item_type": "RadioButtons",
"prototype_item_id": 13516,
"id": 13524,
"item_type": "MatrixQuestionColumnElement",
"alias": null,
"html_class": null,
"include_condition": null
}
}
],
"grid_lines": "None",
"row_text_align": "Left",
"width": null,
"question_text": "<p><span style=\"font-family: Lato;\">This is the Grid Radio Question</span></p>",
"subtext": "",
"is_required": false,
"item_position": "Left",
"question_text_position": "Top",
"id": 13515,
"item_type": "Matrix",
"page_id": 5326,
"position": 1,
"alias": null,
"html_class": null,
"include_condition": null
}}
Notice how it is a two item array with quite difference values in each array element. So, if I have a c# object that matches each of the objects in the array, how do I get JSON to deserialize each array element into their respective types? (There is a base type that has the item_type and text in it). I have this working:
object[] objValues = JsonConvert.DeserializeObject<object[]>(strJson, settings);
baseObject[] objBaseValues = JsonConvert.DeserializeObject<baseObject[]>(strJson, settings);
Then, I'm looking through the objBaseValues to find out which type and then re-converting each array element from the json object, like this:
List<baseObject> objReturn = new List<baseObject>();
for (int i = 0; i < objBaseValues.Length; i++)
{
if (objBaseValues[i].item_type.ToLower() == "matrix")
{
objReturn.Add(JsonConvert.DeserailizeObject<MatrixPageItem>(JsonConver.SerializeObject(objValues[i]), settings);
}
else
{
objReturn.Add(JsonConvert.DeserializeObject<PageItem>(JsonConvert.SerializeObject(objValues[i]), settings);
}
}
This is working, but is there a better way? TIA,
Upvotes: 2
Views: 238
Reputation: 718
Alright! This is probably going to be a long one xD So bear with me.
What I did to resolve this issue was create an object that implemented JsonConverter. What I did with this custom implementation was grab something inside the object that identified what type it was. And then activate the type that matched and called serializer.Populate(reader, instance) to pass the ball back to the default jsonconverter.
This is some of the code that I have in production: I've written some instructions in the comments
public class JsonTypeIdBasedConverter : JsonConverter
{
public override bool CanWrite => false;
public override bool CanRead => true;
//I do not know what this needs to be for you. Because what happens is, the JsonConverter walks over the type specified in the generic parameter.
//It walks over its propertyTypes and passes that type on to this method to decide if this object was meant to parse for the type.
//So what I would suggest is you make a wrapper object representing the list, that has a list of items of type interface for the two or more objects that you want to parse to and decide here of the property is a list of that specific interface
//If you simply do return true, that will not work because then this object will also get called for every other type of parsing, you do not want that :)
public override bool CanConvert(Type objectType)
=> typeof(ResolvableByTypeId).IsAssignableFrom(objectType);
public override void WriteJson(JsonWriter writer, object value, Newtonsoft.Json.JsonSerializer serializer)
=> throw new InvalidOperationException("Use default serialization.");
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, Newtonsoft.Json.JsonSerializer serializer)
{
var jsonToken = JToken.Load(reader);
if (jsonToken.Type == JTokenType.Null)
return null;
//this for me was the identifying attribute in the json object that revealed what type is was for them, so I could grab our
string typeId = jsonToken["TypeId"].Value<string>();
object result = CreateObjectByTypeId(typeId);
serializer.Populate(jsonToken.CreateReader(), result);
return result;
}
private object CreateObjectByTypeId(string typeId)
{
... grab type from somewhere, list? switch?
return Activator.CreateInstance(typeToCreate);
}
}
Serializer.Populate then further walks over all properties of that newly instantiated object and recursively does what it normally does.
The interface then needs to have an attribute that labels it needs to be parsed using this converter type:
namespace SomeNamespace
{
[JsonConverter(typeof(JsonTypeIdBasedConverter))]
public interface Condition
{
bool Validate(Context context);
}
}
I've triple checked my code... I think that was all there is to it. But if I am mistaken, please comment back and i'll run past it all again if this helps you! :)
-- PS:
I do not think that long ass comment in the code is quite transparent... So i'll try to shine some light on what I mean.
I have a class that has a list of type interface (1), since json does not know what concrete type it actually needs to resolve to, I label the interface with a tagging interface(2) and make a custom json converter(3) that recognizes the interface(4), but does know the types it needs to resolve to based on information in the json file.
(1)
public class Wrapper
{
List<AbstractItemType> _unknownType
}
(3)[JsonConverter(typeof(AbstractItemJsonConverter))]
(2)public Interface AbstractItemType : TaggingInterface {}
public Interface TaggingInterface {}
public class ConcreteItem1 : AbstractItemType
{
}
public class ConcreteItem2 : AbstractItemType
{
}
(3)
public class AbstractItemJsonConverter : JsonConverter
{
...
(4)
public override bool CanConvert(Type objectType)
=> typeof(TaggingInterface).IsAssignableFrom(objectType);
...
}
Upvotes: 1