Swift 4 Decodable - decoding JSON object into `Data`
Asked Answered
S

3

6

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?

Seaddon answered 29/10, 2017 at 10:24 Comment(7)
Please show us your JSON input data. From the error you are getting (i.e., ”Expected to decode Array but found a Dictionary instead.”) your JSON format might not be what you expected.Olson
@PauloMattos - that's the point. I want everything that is under the "data" key to be decoded as Data. Regardless of whether its dict, array, number or string.Seaddon
You want the raw data under the ”data" key, no matter the format? If so, looks tricky... ;)Olson
Yes, pure bytes.Seaddon
You can't send plain data in a json string. You would need to use base64encoding. Besides that If you don't provide a valid json to be used as a reference in your question it would be impossible to help.Shudder
Your first problem is trying to decode an array of UntypedObject, when you try and decode a single instance of UntypedObject the error is self explanatory. The correct way to handle your scenario would be to create another class/struct which conforms to Decodable and model the data which you expect to receive in the "data" property.Glaydsglaze
I have the same problem (I need to send the value of the "data" key to a 3rd party library that expects a JSON string, but does not support Decodable). Did you ever find a solution?Effluvium
E
3

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"}
Effluvium answered 22/4, 2020 at 2:24 Comment(0)
T
1

For your error in particular changing

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

to

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

would fix it (but I still don't think you'd be able to get the content of the "data" key on the JSON as Data.)

The reason for your error is because your JSON response contains an object as the root which is seen as a dictionary in Swift (because of the key/value mapping) but you were trying to decode an array of objects instead.

Turbot answered 7/5, 2019 at 14:27 Comment(0)
G
0

When you are decoding it, can you check to see if it is an Array first?

Then your code above works if “true”, else decode for a dictionary

Check out this article on decoding that I found helpful:

https://medium.com/swiftly-swift/swift-4-decodable-beyond-the-basics-990cc48b7375

Greenberg answered 9/9, 2018 at 14:17 Comment(1)
Hello and welcome to SO! Could you please show us an example in code to help further your answers helpfulness? Thanks!Dyer

© 2022 - 2025 — McMap. All rights reserved.