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 var
iables.
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
Swift Equivalent of Unity3D Coroutines
Invalid Update: Invalid Number of Rows in Section 1
Uitableview Didselectrow Returns Wrong Row Index Value
Swiftui New App Lifecycle How to Connect The Facebook Sdk
How to Parse Url # Fragments with Query Items in Swift
Swift Struct Adopting Protocol with Static Read-Write Property Doesn't Conform
Editable Nstextview from Interface Builder
What Is This Syntax: Func Funcname(Stuff1)(Stuff2)->Returntype {}
Create Custom Action in a Class for Use in Interface Builder
Why Does User Defaults Publisher Trigger Multiple Times
Using a Timer in The Background Thread to Update UI
How Do Uniqueness Constraints as (Comma,Separated,Attributes) Work with Swift in Coredata
Urlcomponents Queryitems Losing Percent Encoding When Mutated
How to Get Unsaferawpointer on The Swift Object