Home > front end >  Codable Swift Structs for JSON with Nested Empty Array
Codable Swift Structs for JSON with Nested Empty Array

Time:03-08

I have the following Swift code with some sample JSON that I'm trying to decode. You can drop this into a Playground to try it for yourself:

let json = """
{
  "sessionState": "abc",
  "methodResponses": [
    [
      "Mailbox/get",
      {
        "state": "92",
        "accountId": "xyz"
      },
      "0"
    ]
  ]
}
"""

let data = json.data(using: .utf8)!

if let object = try? JSONDecoder().decode(JMAPResponse.self, from: data) {
  print(object)
}else{
  print("JMAP decode failed")
}

I had no idea how to handle the nested array, so I used quicktype.io, but what it generated still doesn't work.

Here is my root element with the nested array:

//Root
struct JMAPResponse: Codable{
  var sessionState: String?
  var methodResponses: [[JMAPResponseChild]]
}

Here is the enum that QuickType suggested I use to process the methodResponse node:

//Nested array wrapper
enum JMAPResponseChild: Codable{
  case methodResponseClass(JMAPMethodResponse)
  case string(String)

  init(from decoder: Decoder) throws {
      let container = try decoder.singleValueContainer()
      if let x = try? container.decode(String.self) {
        self = .string(x)
        return
      }
      if let x = try? container.decode(JMAPMethodResponse.self) {
        self = .methodResponseClass(x)
        return
      }
      throw DecodingError.typeMismatch(JMAPResponseChild.self, DecodingError.Context(codingPath: decoder.codingPath, debugDescription: "Wrong type for JMAPResponseChild"))
  }

  func encode(to encoder: Encoder) throws {
    var container = encoder.singleValueContainer()
    switch self {
    case .methodResponseClass(let x):
        try container.encode(x)
    case .string(let x):
        try container.encode(x)
    }
  }
}

Here's the next level down:

//No-key data structure (Mailbox/get, {}, 0)
struct JMAPMethodResponse: Codable{
  var method: String
  var data: [JMAPMailboxList]
  var id: String 

  func encode(to encoder: Encoder) throws {
    var container = encoder.unkeyedContainer()
    try container.encode(method)
    try container.encode(data)
    try container.encode(id)
  }
}

And finally, the lowest-level node:

struct JMAPMailboxList: Codable{
  var state: String?
  var accountId: String?
}

It still can't decode the structure successfully. Can anyone see what I'm doing wrong?

CodePudding user response:

Your JMAPResponseChild is decoding an array expecting either a String or JMAPMethodResponse. The array looks like this:

    [
      "Mailbox/get",
      {
        "state": "92",
        "accountId": "xyz"
      },
      "0"
    ]

It actually contains either a String or JMAPMailboxList (not JMAPMethodResponse). If you change all the JMAPMethodResponse references in JMAPResponseChild to JMAPMailboxList it should work. You will have to do some post processing or change your code to translate that array of three values into JMAPMethodResponse.

You might structure your JMapMethodResponse more like this:

struct JMAPMethodResponse: Codable {
  var method: String
  var data: JMAPMailboxList
  var id: String

  init(from decoder: Decoder) throws {
    let container = try decoder.singleValueContainer()
    let values = try container.decode([JMAPResponseChild].self)

    guard case .string(let extractedMethod) = values[0] else {
      throw DecodingError.typeMismatch(String.self, DecodingError.Context(codingPath: decoder.codingPath, debugDescription: "First array element not String"))
    }
    method = extractedMethod
    guard case .methodResponseClass(let extractedData) = values[1] else {
      throw DecodingError.typeMismatch(JMAPMailboxList.self, DecodingError.Context(codingPath: decoder.codingPath, debugDescription: "Second array element not JMAPMailboxList"))
    }
    data = extractedData
    guard case .string(let extractedId) = values[2] else {
      throw DecodingError.typeMismatch(String.self, DecodingError.Context(codingPath: decoder.codingPath, debugDescription: "Third array element not String"))
    }
    id = extractedId
  }

  func encode(to encoder: Encoder) throws {
    var container = encoder.unkeyedContainer()
    try container.encode(JMAPResponseChild.string(method))
    try container.encode(JMAPResponseChild.methodResponseClass(data))
    try container.encode(JMAPResponseChild.string(id))
  }
}

Then you can just get an array of those in your response:

struct JMAPResponse: Codable{
  var sessionState: String?
  var methodResponses: [JMAPMethodResponse]
}

I don't know if the encoder or decoder guarantee ordering in the array. If they don't this could randomly break.

  • Related