Decoding Different Type with and Without Array

Decoding objects without knowing their type first

I honestly haven't used the new any syntax enough to know if that can help but I have done what you're trying to do numerous times and here is how I do it.

Set up the data first

We first declare what a Zombie and a Skeleton are. They could just inherit from a protocol or they could be separate structs...

struct Zombie: Decodable {
let someProperty: Int
}

struct Skeleton: Decodable {
let someProperty: Int
let skeletonSpecificProperty: String
}

Then we can turn your array of [anyEntityType] into a homogeneous array by using an enum and embedding the entities into it...

enum Entity: Decodable {
case zombie(Zombie)
case skeleton(Skeleton)
}

Decode the enum given your JSON structure

We have to provide a custom decoder for the Entity type...

init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: RootKeys.self)

// First get the `id` value from the JSON object
let type = try container.decode(String.self, forKey: .id)

// check the value for each type of entity we can decode
switch type {

// for each value of `id` create the related type
case "zombie":
let zombie = try Zombie(from: decoder)
self = .zombie(zombie)
case "skeleton":
let skeleton = try Skeleton(from: decoder)
self = .skeleton(skeleton)
default:
// throw an error here... unsupported type or something
}
}

This should now let you decode an array of Entities from JSON into an [Entity] array.

Deal with "unknown" types

There is an extra step required for dealing with the "unknown" types. For instance, in the code above. If the JSON contains "id": "creeper" this will error as it can't deal with that. And you'll end up with your whole array failing to decode.

I've created a couple of helper functions that help with that...

If you create an object like...

struct Minecraft: Decodable {
let entities: [Entity]

enum RootKeys: String, CodingKey {
case entities
}
}

And these helpers...

extension KeyedDecodingContainer {
func decodeAny<T: Decodable>(_ type: T.Type, forKey key: K) throws -> [T] {
var items = try nestedUnkeyedContainer(forKey: key)

var itemsArray: [T] = []
while !items.isAtEnd {
guard let item = try? items.decode(T.self) else {
try items.skip()
continue
}
itemsArray.append(item)
}
return itemsArray
}
}

private struct Empty: Decodable { }

extension UnkeyedDecodingContainer {
mutating func skip() throws {
_ = try decode(Empty.self)
}
}

You can create a custom decoder for the Minecraft type like this...

init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: RootKeys.self)
self.entities = try container.decodeAny(Entity.self, forKey: .entities)
}

Custom JSON decoding: decode an array of different objects in several distinct arrays

This is a solution with nested containers. With the given (simplified but valid) JSON string

let jsonString = """
{
"zones": [
{
"name": "zoneA",
"blocks": [
{"name": "Foo"}
]
},
{
"name": "zoneB",
"blocks": [
{"street":"Broadway", "city":"New York"}
]
},
{
"name": "zoneC",
"blocks": [
{"email": "foo@bar.com"}
]
},
{
"name": "zoneD",
"blocks": [
{"phone": "555-01234"}
]
}
]
}
"""

and the corresponding element structs

struct ElementA : Decodable { let name: String }
struct ElementB : Decodable { let street, city: String }
struct ElementC : Decodable { let email: String }
struct ElementD : Decodable { let phone: String }

first decode the zones as nestedUnkeyedContainer then iterate the array and decode first the name key and depending on name the elements.

Side note: This way requires to declare the element arrays as variables.

struct Root : Decodable {
var elementsA = [ElementA]()
var elementsB = [ElementB]()
var elementsC = [ElementC]()
var elementsD = [ElementD]()

enum Zone: String, Decodable { case zoneA, zoneB, zoneC, zoneD }

private enum CodingKeys: String, CodingKey { case zones }
private enum ZoneCodingKeys: String, CodingKey { case name, blocks }

init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
var zonesContainer = try container.nestedUnkeyedContainer(forKey: .zones)
while !zonesContainer.isAtEnd {
let item = try zonesContainer.nestedContainer(keyedBy: ZoneCodingKeys.self)
let zone = try item.decode(Zone.self, forKey: .name)
switch zone {
case .zoneA: elementsA = try item.decode([ElementA].self, forKey: .blocks)
case .zoneB: elementsB = try item.decode([ElementB].self, forKey: .blocks)
case .zoneC: elementsC = try item.decode([ElementC].self, forKey: .blocks)
case .zoneD: elementsD = try item.decode([ElementD].self, forKey: .blocks)
}
}
}
}

Decoding the stuff is straightforward

do {
let result = try JSONDecoder().decode(Root.self, from: Data(jsonString.utf8))
print(result)
} catch {
print(error)
}

How to decode array of different data types?

Any is not supported in Codable, nor AnyObject nor AnyClass

A possible solution is an unkeyedContainer and an enum with associated values

let mockJSON =
"""
{
"arr" : [
0,"my_str1",
90,"my_str2"
]
}
"""

enum MockType {
case int(Int), string(String)
}

struct Mock: Decodable {

var values = [MockType]()

init(from decoder: Decoder) throws {
var container = try decoder.unkeyedContainer()
while !container.isAtEnd {
do {
let intValue = try container.decode(Int.self)
values.append(.int(intValue))
} catch DecodingError.typeMismatch {
let stringValue = try container.decode(String.self)
values.append(.string(stringValue))
}
}
}
}

let data = Data(mockJSON.utf8)

do {
let decoder = JSONDecoder()
let result = try decoder.decode([String:Mock].self, from: data)
print(result["arr"]!.values)
} catch {
print(error)
}

Custom Decodable type that can be an Array or a String

The issue here is that singleValueContainer can be used to decode also an array. So, the error that you are getting is produced by the second try inside init(from:) function of TitleStringValue and not before.

Having said that, you can further simplify your custom decoding like this:

struct TitleStringValue: Decodable {
let text: String

struct TitleStringValueInner: Decodable {
let text: String
}

init(from decoder: Decoder) throws {
let container = try decoder.singleValueContainer()
if let string = try? container.decode([TitleStringValueInner].self).first?.text {
text = string
} else {
text = try container.decode(String.self)
}
}
}

How to decode from JSON array of different objects that conforms to the same protocol

Eventually (thanks to @Sweeper) the final solution looks like this

import Cocoa
import SceneKit

extension SCNVector3 {
func toArr() -> [Float] {
return [Float(self.x), Float(self.y), Float(self.z)]
}
}

extension Array where Element == Float {
func toSCNVector3() -> SCNVector3? {
guard self.count <= 3 else { return nil }
return SCNVector3Make(CGFloat(self[0]), CGFloat(self[1]), CGFloat(self[2]))
}
}

protocol Parsable : Codable {
var innerObj: InnerObject? { get set }
var type: Types { get set }
}

protocol FirstImpl: Parsable {
var name: String? { get set }
}

protocol SecondImpl: Parsable {
var name: String? { get set }
}

enum Types: String, Codable {
case first, second
}

class InnerObject: Codable {
var vector: SCNVector3? = SCNVector3.init(3.1, 4.1, 5.1)

enum CodingKeys: CodingKey {
case vector
}

func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(vector?.toArr(), forKey: .vector)
}

init() { }

required init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
vector = try container.decode([Float].self, forKey: .vector).toSCNVector3()
}
}

class ViewController: NSViewController {

override func viewDidLoad() {
super.viewDidLoad()
print("")
// Do any additional setup after loading the view.
foo()
}

override var representedObject: Any? {
didSet {
// Update the view, if already loaded.
}
}

struct AnyEncodable: Encodable {
let encodeFunction: (Encoder) throws -> Void

init(_ encodable: Encodable) {
encodeFunction = encodable.encode(to:)
}

func encode(to encoder: Encoder) throws {
try encodeFunction(encoder)
}
}

struct ParcerableDecodable: Decodable {
private(set) var parerableArr = [Parsable]()

init(from decoder: Decoder) throws {
var container: UnkeyedDecodingContainer = try decoder.unkeyedContainer()

while !container.isAtEnd {
if let obj = try? container.decode(One.self) {
parerableArr.append(obj)
}
else if let obj = try? container.decode(Second.self) {
parerableArr.append(obj)
}
else {
//MARK: no match
}
}
}
}

class One: FirstImpl {
var type: Types = .first

var name: String? = "first"
var innerObj: InnerObject? = InnerObject()

enum CodingKeys: CodingKey {
case name, innerObj, type
}

func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(type, forKey: .type)
try container.encode(name, forKey: .name)
try container.encode(innerObj, forKey: .innerObj)
}

init() { }

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

guard let type = try? container.decode(Types.self, forKey: .type) else {
let context = DecodingError.Context(codingPath: decoder.codingPath, debugDescription: "ERROR: no type field in the given object", underlyingError: nil)
throw DecodingError.typeMismatch(One.self, context)
}

if type != self.type {
let context = DecodingError.Context(codingPath: decoder.codingPath, debugDescription: "Type \(type), doesn't match to needed one \(type), so skip:)", underlyingError: nil)
throw DecodingError.typeMismatch(One.self, context)
}

name = try container.decode(String.self, forKey: .name)
innerObj = try container.decode(InnerObject.self, forKey: .innerObj)
}
}

class Second: SecondImpl {
var type: Types = .second
var innerObj: InnerObject? = InnerObject()
var name: String? = "second"
}

private func foo() {
let first: Codable = One()
let second: Codable = Second()


let arr = [first, second]
let data: Data? = try? JSONEncoder().encode(arr.map(AnyEncodable.init))
let stringFist = String(data: data!, encoding: .utf8)!
print(stringFist)

let decoder = JSONDecoder()
let arrayBack: ParcerableDecodable? = try? decoder.decode(ParcerableDecodable.self, from: data!)
print(arrayBack?.parerableArr.count)
}
}

Why Expected to decode Data but found an array instead I have no arrays in my code

I'm sure this is a bug, it seems like the issue is during the encoding process, when encoding the UIImagePNGRepresentation data, which is returned as Optional things go really wrong, if you change the encode method to force the data like this:

container.encode(UIImagePNGRepresentation(image)!, forKey: .image)

instead of:

container.encode(UIImagePNGRepresentation(image), forKey: .image)

Things will work both in Xcode 9.1 and 9.2, if you leave the data optional as it is, the encoding will work only on 9.2 and fail 9.1, for some reason the image data size will be doubled on 9.1.

I'm pretty sure it's this bug report on swift.org

Which also mentions that it causes memory usage double the size of the data, which is what I'm seeing on my local setup.

So the easy way around is to force the UIImagePNGRepresentation(..) data or upgrade to 9.2 where this bug is fixed.

Hope this answered your question, very interesting find indeed!

How can I decode different objects in the same array using Swift Decoder?

What you want to do is to first decode type then use a switch over the decoded value and init the corresponding type using the same decoder object.

You can also shorten the CodingKeys enum because of this since we are only using one key.

enum CodingKeys: String, CodingKey {
case type
}

init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)

let type = try container.decode(String.self, forKey: .type)
switch type {
case "like":
self = try .like(LikeActivity(from: decoder))
case "comment":
self = try .comment(CommentActivity(from: decoder))
case "follow":
self = try .follow(FollowActivity(from: decoder))
default:
throw DecodingError.wrongJSON
}
}

You could also create an enum for the type values

enum ActivityType: String {
case like, comment, follow
}

Then the switch in the init(from:) becomes

switch ActivityType(rawValue: type) {
case .like:
self = try .like(LikeActivity(from: decoder))
case .comment:
self = try .comment(CommentActivity(from: decoder))
case .follow:
self = try .follow(FollowActivity(from: decoder))
default:
throw DecodingError.wrongJSON
}


Related Topics



Leave a reply



Submit