1

I'm using JSON.NET to deserialize JSON responses from HTTP queries, but I have a problem on deserialization.

The source JSON is like that:

[
  {
    "type": "rpc",
    "tid": 18,
    "action": "TaskSystem",
    "method": "createTask",
    "result": {
      "0": {
        "success": true,
        "successes": [
          [
            "Task successfuly created with S/N #22920"
          ]
        ]
      },
      "1": {
        "success": true,
        "successes": [
          [
            "Task successfuly created with S/N #22921"
          ],
          "Task #22921 marked as urgent"
        ]
      },
      "records": [
        {
          "id": 22920
        },
        {
          "id": 22921
        }
      ],
      "success": true
    }
  }
]

I've been using these classes for deserialization:

Private Sub Deserialize()
    Dim Jobj = Newtonsoft.Json.JsonConvert.DeserializeObject(Of Response())(Jstring)
End Sub

Public Class Record
    Public Property id As Integer
End Class

Public Class Result
    Public Property records As Record()
    Public Property success As Boolean
End Class

Public Class Response
    Public Property type As String
    Public Property tid As Integer
    Public Property action As String
    Public Property method As String
    Public Property result As Result
End Class

But then I am losing the success/failure messages returned by the query

How should I write the class Result in order to colect the properties records As Record(), succes As Boolean and also those objects named "0", "1" and so on...?

Thank you very much for any help.

Ňɏssa Pøngjǣrdenlarp
  • 38,411
  • 12
  • 59
  • 178
VBobCat
  • 2,527
  • 4
  • 29
  • 56
  • There is a missing comma after `[ "Task successfuly ... #22920" ]` which results in the `List(Of Object)` version. Can you check that is correct? – Ňɏssa Pøngjǣrdenlarp May 23 '16 at 22:38
  • I checked my logs and there is no comma in case of #22920. If not beautified, that part of the JSON string is `"successes": [ [ "Task successfuly created with S/N #22920" ] ]`, which is odd. But then again, if these responses were really well-formed, I think I wouldn't have these problems... – VBobCat May 23 '16 at 22:44
  • 1
    LOL. You might want to create a converter and force both those dudes to the same type, otherwise the answer has a quick and dirty workaround – Ňɏssa Pøngjǣrdenlarp May 23 '16 at 22:47
  • what about the inverse: does 22921 have the comma? – Ňɏssa Pøngjǣrdenlarp May 23 '16 at 22:48
  • Yup :-( ... How/where can I learn the basics to write custom converters for dealing with issues such as those? – VBobCat May 23 '16 at 22:50
  • They are not hard - Json.NET gives you what is essentially a Dictionary and you can pull out what you want and store it how you want. I know I have at least 1 answer here, but of course I cant find it. Google `JsonConverter` – Ňɏssa Pøngjǣrdenlarp May 23 '16 at 22:56
  • Give it a shot and post a new question if you get stuck. I have a quickie that converts both types to a simple List(of String). Its actually 2 converters since they are structured differently. Do you have any data with more than 1-2 items in it...better for testing – Ňɏssa Pøngjǣrdenlarp May 23 '16 at 23:41

2 Answers2

2

You can copy your JSON to the clipboard and use
Edit -> Paste Special -> Paste JSon As Classes
to get the rough out of the classes. All the robots get parts of it wrong such as:

' wrong
Public Property successes()() As String
' correct:
Public Property successes As String()()

This seems to work:

Public Class StatusS          ' string()() version
    Public Property success As Boolean
    Public Property successes As List(Of List(Of String))
End Class

Public Class StatusO         ' Object() version
    Public Property success As Boolean
    Public Property successes As List(Of Object)
End Class

The robots also have a hard time with illegal property names, especially with VB where there are lots of keywords (End and Error being common). Use JsonProperty to create an alias (map "0" or "1" to something legal):

Public Class Result
    <JsonProperty("0")>
    Public Property StatusA As StatusS
    <JsonProperty("1")>
    Public Property StatusB As StatusO
    Public Property records As List(Of Record)
    Public Property success As Boolean
End Class

Anywhere that they offer an array, you can use a List(Of T) instead.

Dim jData = JsonConvert.DeserializeObject(Of Response())(jstr)

Console.WriteLine(jData(0).action)
Console.WriteLine(jData(0).result.records(0).id)
Console.WriteLine(jData(0).result.StatusA.success)
Console.WriteLine(jData(0).result.StatusA.successes(0)(0).ToString)
Console.WriteLine(jData(0).result.StatusB.successes(0).ToString)

Result:

TaskSystem
22920
True
Task successfuly created with S/N #22920
[
"Task successfuly created with S/N #22921"
]

It is hard to spot but a missing/extra comma after [ "Task successfuly ... #22920" ] which results in the Object version vs String()() one or the other is likely a typo. If not, you could write a converter for both of those, or add a method to the StatusO class for a Q+D means to remove the brackets:

Friend Function GetSuccessMsg(ndx As Int32) As String
    If ndx < successes.Count Then
        Return successes(ndx).ToString().
            Replace("[", "").Replace("]", "").
            Replace("""", "").Trim()
    End If
    Return String.Empty
End Function

Console.WriteLine(jData(0).result.StatusA.GetSuccessMsg(0))

Task successfuly created with S/N #22921

Ňɏssa Pøngjǣrdenlarp
  • 38,411
  • 12
  • 59
  • 178
1

You have two unrelated problems here:

  1. Your Result class consists of a set of fixed properties and a set of variable properties with incremented numeric names and a standardized schema for the values. You would like to deserialize the standard properties automatically and capture and deserialize the custom properties as well.

    This can be done using JsonExtensionData. With this attribute, you can temporarily deserialize the custom properties to a Dictionary(of String, JToken) then later convert to a Dictionary(Of String, Success) in an [OnDeserialized] callback. Here Success is to be a type designed to capture JSON like this:

    {
      "success": true,
      "successes": [ [ "Task successfuly created with S/N #22920" ] ]
    }
    

    For documentation, see Deserialize ExtensionData.

  2. Inside the abovementioned Success type, the "successes" array contains both arrays of strings and individual strings.

    If you define your successes property as follows:

    Public Property successes As List(Of List(Of String))
    

    Then this can be handled using a variation of the SingleOrArrayConverter(Of String) from How to handle both a single item and an array for the same property using JSON.net, setting it as the item converter via <JsonProperty(ItemConverterType := GetType(SingleOrArrayConverter(Of String)))>.

Thus your final classes would look like:

Public Class Success
    Public Property success As Boolean
    <JsonProperty(ItemConverterType := GetType(SingleOrArrayConverter(Of String)))> _   
    Public Property successes As List(Of List(Of String))
End Class

Public Class Record
    Public Property id As Integer
End Class

Public Class Result
    Public Property records As Record()
    Public Property success As Boolean

    <JsonIgnore> _
    Public Property successes as Dictionary(Of string, Success)

    <JsonExtensionData> _
    Private _additionalData as Dictionary(Of string, JToken)

    <System.Runtime.Serialization.OnSerializing> _
    Sub OnSerializing(ByVal context as System.Runtime.Serialization.StreamingContext)
        If successes IsNot Nothing
            _additionalData = successes.ToDictionary(Function(p) p.Key, Function(p) JToken.FromObject(p.Value))
        Else
            _additionalData = Nothing
        End If
    End Sub

    <System.Runtime.Serialization.OnSerialized> _
    Sub OnSerialized(ByVal context as System.Runtime.Serialization.StreamingContext)
        _additionalData = Nothing
    End Sub

    <System.Runtime.Serialization.OnDeserializing> _
    Sub OnDeserializing(ByVal context as System.Runtime.Serialization.StreamingContext)
        _additionalData = Nothing
    End Sub

    <System.Runtime.Serialization.OnDeserialized>
    Sub OnDeserialized(ByVal context as System.Runtime.Serialization.StreamingContext)

        If _additionalData IsNot Nothing
            successes = _additionalData.ToDictionary(Function(p) p.Key, Function(p) p.Value.ToObject(Of Success)())
        End If
        _additionalData = Nothing

    End Sub
End Class

Public Class Response
    Public Property type As String
    Public Property tid As Integer
    Public Property action As String
    Public Property method As String
    Public Property result As Result
End Class

Public Class SingleOrArrayConverter(Of T)
    Inherits JsonConverter

    Public Overrides ReadOnly Property CanWrite() As Boolean
        Get
            Return false
        End Get
    End Property

    Public Overrides Sub WriteJson(writer As JsonWriter, value As Object, serializer As JsonSerializer)
        Throw New NotImplementedException()
    End Sub

    Public Overrides Function ReadJson(reader As JsonReader, objectType As Type, existingValue As Object, serializer As JsonSerializer) As Object
        Dim retVal As Object = New List(Of T)()
        If reader.TokenType = JsonToken.StartArray Then
            serializer.Populate(reader, retVal)
        Else
            Dim instance As T = DirectCast(serializer.Deserialize(reader, GetType(T)), T)
            retVal.Add(instance)
        End If
        Return retVal
    End Function

    Public Overrides Function CanConvert(objectType As Type) As Boolean
        Return objectType = GetType(List(Of T))
    End Function

End Class

Prototype fiddle.

dbc
  • 104,963
  • 20
  • 228
  • 340
  • Thank you very much, this deserialized data correctly and completely and solved my problem. Besides, I learned things I didn't know about capabilities of JSON.Net. I'll try to solve those problems myself in the future :-) – VBobCat May 24 '16 at 13:37