1

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?

2
  • 1
    Never print a hard coded string in the catch clause, print the actual error instead. catch { print(error) }. Commented Mar 6, 2022 at 7:35
  • Great point, thank you. I typically do in other areas, but overlooked it with encoding JSON because sometimes the errors are cryptic and not helpful. 🙂 Commented Mar 7, 2022 at 16:37

1 Answer 1

1

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.

Sign up to request clarification or add additional context in comments.

2 Comments

It will be decoded in the order it appears in the json, that is my understanding.
This is very helpful, thank you! I'm still studying your code to wrap my head around it, but it's making sense and is working well.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.