Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Swift 4 Decodable - decoding JSON object into `Data`

I have the following data structure:

{
    "type": "foo"
    "data": { /* foo object */ }
}

Here's my class for decoding it:

final public class UntypedObject: Decodable {

    public var data: Data

    enum UntypedObjectKeys: CodingKey {
        case data
    }

    required public init(from decoder: Decoder) throws {
        let values = try decoder.container(keyedBy: UntypedObjectKeys.self)

        self.data = try values.decode(Data.self, forKey: .data)
    }
}

I am fetching an array of such objects and this is how I am decoding it:

let decoder = JSONDecoder()
let objectList = try decoder.decode([UntypedObject].self, from: data)

However I am receiving this error in the console:

typeMismatch(Swift.Array, Swift.DecodingError.Context(codingPath: [Foundation.(_JSONKey in _12768CA107A31EF2DCE034FD75B541C9)(stringValue: "Index 0", intValue: Optional(0)), Playground_Sources.UntypedObject.UntypedObjectKeys.data], debugDescription: "Expected to decode Array but found a dictionary instead.", underlyingError: nil))

So the question would be is it possible at all to decode proper JSON object into a Data typed attribute and if so - how can I achieve this?

like image 361
Eimantas Avatar asked Oct 29 '17 10:10

Eimantas


1 Answers

This is likely too late for the OP, but I had a similar problem that needed a solution. Hopefully this is useful for others.

For my problem, I wanted to use Decodable to decode my JSON with Swift 5.1. However at various points in my object hierarchy, I wanted to return an objC object (from a third party library) that did not support Decodable, but did support decoding from a (non-trivial) JSON string. I solved the problem by using JSONSerialization to create an untyped object hierarchy I could retrieve from the decoder's userInfo property and search with the decoder's contextPath to find my data, and then use JSONSerialization to convert it back to string data.

This solution makes no assumption about the object/array hierarchy required to get to the object that has the "data" key.

// Swift 5.1 Playground
import Foundation

// Input configuration JSON
let jsonStr = """
{
  "foo":"bar",
  "bars": [
    {
      "data":{
        "thing1":"#111100",
        "thing2":12
      }
    },
    {
      "data":{
        "thing1":"#000011",
        "thing2":64.125
      }
    }
  ]
}
"""

// Object passed to the decoder in the UserInfo Dictionary
// This will contain the serialized JSON data for use by
// child objects
struct MyCodingOptions {
  let json: Any

  static let key = CodingUserInfoKey(rawValue: "com.unique.mycodingoptions")!
}

let jsonData = Data(jsonStr.utf8)
let json = try JSONSerialization.jsonObject(with: jsonData)
let options = MyCodingOptions(json: json)

let decoder = JSONDecoder()
decoder.userInfo = [MyCodingOptions.key: options]

// My object hierarchy
struct Root: Decodable {
  let foo: String
  let bars: [Bar]
}

struct Bar: Decodable {
  let data: Data?

  enum CodingKeys: String, CodingKey {
    case data = "data"
  }
}

// Implement a custom decoder for Bar
// Use the context path and the serialized JSON to get the json value
// of "data" and then deserialize it back to data.
extension Bar {
  init(from decoder: Decoder) throws {
    var data: Data? = nil
    if let options = decoder.userInfo[MyCodingOptions.key] as? MyCodingOptions {
      // intialize item to the whole json object, then mutate it down the "path" to Bar
      var item: Any? = options.json
      let path = decoder.codingPath // The path to the current object, does not include self
      for key in path {
        if let intKey = key.intValue {
          //array
          item = (item as? [Any])?[intKey]
        } else {
          //object
          item = (item as? [String:Any])?[key.stringValue]
        }
      }
      // item is now Bar, which is an object (Dictionary)
      let bar = item as? [String:Any]
      let dataKey = CodingKeys.data.rawValue
      if let item = bar?[dataKey] {
        data = try JSONSerialization.data(withJSONObject: item)
      }
    }
    self.init(data: data)
  }
}

if let root = try? decoder.decode(Root.self, from: jsonData) {
  print("foo: \(root.foo)")
  for (i, bar) in root.bars.enumerated() {
    if let data = bar.data {
      print("data #\(i): \(String(decoding: data, as: UTF8.self))")
    }
  }
}

//prints:
// foo: bar
// data #0: {"thing2":12,"thing1":"#111100"}
// data #1: {"thing2":64.125,"thing1":"#000011"}
like image 182
Regan Sarwas Avatar answered Nov 18 '22 20:11

Regan Sarwas