Swift Codable with Different Array Types

Swift Codable with Different Array of Dictionary Types

First, you need to create a third struct that can represent the array, for example:

struct Response : Codable {
var dashboardSummary: DashboardSummary?
var daySummaries: [DaySummary]

enum CodingKeys: CodingKey {
case dashboardSummary, daySummaries
}
}

Then you need to implement init(from:) and encode(to:) to do custom coding.

You can use an unkeyedContainer to decode/encode it. When decoding, you keep decoding from the container while the container is not isAtEnd.

init(from decoder: Decoder) throws {
var container = try decoder.unkeyedContainer()
dashboardSummary = try container.decodeIfPresent(DashboardSummary.self)
daySummaries = []
while !container.isAtEnd {
daySummaries.append(try container.decode(DaySummary.self))
}
}

func encode(to encoder: Encoder) throws {
var container = encoder.unkeyedContainer()
try container.encode(dashboardSummary)
for daySummary in daySummaries {
try container.encode(daySummary)
}
}

You can also consider making the properties let constants if you don't intend on changing them.

Swift Codable - Parse JSON array which can contain different data type

I used quicktype to infer the type of config_data and it suggested an enum with separate cases for your object, string, and integer values:

struct ConfigData {
let configData: [ConfigDatumElement]
}

enum ConfigDatumElement {
case configDatumClass(ConfigDatumClass)
case integer(Int)
case string(String)
}

struct ConfigDatumClass {
let name, configTitle: String
}

Here's the complete code example. It's a bit tricky to decode the enum but quicktype helps you out there:

// To parse the JSON, add this file to your project and do:
//
// let configData = try? JSONDecoder().decode(ConfigData.self, from: jsonData)

import Foundation

struct ConfigData: Codable {
let configData: [ConfigDatumElement]

enum CodingKeys: String, CodingKey {
case configData = "config_data"
}
}

enum ConfigDatumElement: Codable {
case configDatumClass(ConfigDatumClass)
case integer(Int)
case string(String)

init(from decoder: Decoder) throws {
let container = try decoder.singleValueContainer()
if let x = try? container.decode(Int.self) {
self = .integer(x)
return
}
if let x = try? container.decode(String.self) {
self = .string(x)
return
}
if let x = try? container.decode(ConfigDatumClass.self) {
self = .configDatumClass(x)
return
}
throw DecodingError.typeMismatch(ConfigDatumElement.self, DecodingError.Context(codingPath: decoder.codingPath, debugDescription: "Wrong type for ConfigDatumElement"))
}

func encode(to encoder: Encoder) throws {
var container = encoder.singleValueContainer()
switch self {
case .configDatumClass(let x):
try container.encode(x)
case .integer(let x):
try container.encode(x)
case .string(let x):
try container.encode(x)
}
}
}

struct ConfigDatumClass: Codable {
let name, configTitle: String

enum CodingKeys: String, CodingKey {
case name
case configTitle = "config_title"
}
}

It's nice to use the enum because you get the most type-safety that way. The other answers seem to lose this.

Using quicktype's convenience initializers option, a working code sample is:

let data = try ConfigData("""
{
"config_data": [
{
"name": "illuminate",
"config_title": "Blink"
},
{
"name": "shoot",
"config_title": "Fire"
},
"illuminate",
"shoot",
25,
100
]
}
""")

for item in data.configData {
switch item {
case .configDatumClass(let d):
print("It's a class:", d)
case .integer(let i):
print("It's an int:", i)
case .string(let s):
print("It's a string:", s)
}
}

This prints:

It's a class: ConfigDatumClass(name: "illuminate", configTitle: "Blink")
It's a class: ConfigDatumClass(name: "shoot", configTitle: "Fire")
It's a string: illuminate
It's a string: shoot
It's an int: 25
It's an int: 100

Handling JSON Array Containing Multiple Types - Swift 4 Decodable

I figured out how to decode the mixed included array into two arrays of one type each. Using two Decodable structs is easier to deal with, and more versatile, than having one struct to cover multiple types of data.

This is what my final solution looks like for anyone who's interested:

struct Root: Decodable {
let data: [Post]?
let members: [Member]
let images: [ImageMedium]

init(from decoder: Decoder) throws {

let container = try decoder.container(keyedBy: CodingKeys.self)

data = try container.decode([Post].self, forKey: .data)

var includedArray = try container.nestedUnkeyedContainer(forKey: .included)
var membersArray: [Member] = []
var imagesArray: [ImageMedium] = []

while !includedArray.isAtEnd {

do {
if let member = try? includedArray.decode(Member.self) {
membersArray.append(member)
}
else if let image = try? includedArray.decode(ImageMedium.self) {
imagesArray.append(image)
}
}
}
members = membersArray
images = imagesArray
}

enum CodingKeys: String, CodingKey {
case data
case included
}
}

struct Post: Decodable {
let id: String?
let type: String?
let title: String?
let ownerId: String?
let ownerType: String?

enum CodingKeys: String, CodingKey {
case id
case type
case title
case ownerId = "owner-id"
case ownerType = "owner-type"
}
}

struct Member: Decodable {
let id: String?
let type: String?
let firstName: String?
let lastName: String?

enum CodingKeys: String, CodingKey {
case id
case type
case firstName = "first-name"
case lastName = "last-name"
}
}

struct ImageMedium: Decodable {
let id: String?
let type: String?
let assetUrl: String?
let ownerId: String?
let ownerType: String?

enum CodingKeys: String, CodingKey {
case id
case type
case assetUrl = "asset-url"
case ownerId = "owner-id"
case ownerType = "owner-type"
}
}

How can i parse an Json array of a list of different object using Codable?

A reasonable solution is an enum with associated values because the type can be determined by the productype key. The init method first decodes the productype with a CodingKey then in a switch it decodes (from a singleValueContainer) and associates the proper type/value to the corresponding case.

enum ProductType: String, Codable {
case a, b, c
}

struct Root : Codable {
let items : [Product]
}

struct ProductA : Codable {
let id, name: String
let productype: ProductType
let propertyOfA : String
}

struct ProductB : Codable {
let id, name: String
let productype: ProductType
let propertyOfB : String
}

struct ProductC : Codable {
let id, name: String
let productype: ProductType
let propertyOfC, propertyOfC2 : String
}

enum Product : Codable {

case a(ProductA), b(ProductB), c(ProductC)

enum CodingKeys : String, CodingKey { case productype }

init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
let type = try container.decode(ProductType.self, forKey: .productype)
let singleContainer = try decoder.singleValueContainer()
switch type {
case .a : self = .a(try singleContainer.decode(ProductA.self))
case .b : self = .b(try singleContainer.decode(ProductB.self))
case .c : self = .c(try singleContainer.decode(ProductC.self))
}
}

func encode(to encoder: Encoder) throws {
var container = encoder.singleValueContainer()
switch self {
case .a(let productA): try container.encode(productA)
case .b(let productB): try container.encode(productB)
case .c(let productC): try container.encode(productC)
}
}
}

And decode

let jsonString = """
{
"items": [
{
"id": "1",
"name": "name",
"propertyOfA": "1243",
"productype": "a"

},
{
"id": "2",
"name": "name",
"propertyOfA": "12",
"productype": "a"
},
{
"id": "3",
"name": "name",
"propertyOfA": "1243",
"productype": "a"
},
{
"id": "1",
"name": "name",
"propertyOfB": "1243",
"productype": "b"
},
{
"id": "1",
"name": "name",
"propertyOfC": "1243",
"propertyOfC2": "1243",
"productype": "c"
}
]
}
"""
do {
let result = try JSONDecoder().decode(Root.self, from: Data(jsonString.utf8))
print(result)
} catch { print(error)}

To read the enum values use also a switch.

Generic Codable types

I have done something similar to this in the past. Not with Firestore (although, more recently I did) but with our CMS that we use.

As @vadian pointed out, heterogeneous arrays are not supported by Swift.

Also... something else to point out.

When you have a generic type defined like...

struct Submission<Cell> {
let cells: [Cell]
}

Then, by definition, cells is a homogeneous array of a single type. If you try to put different types into it it will not compile.

You can get around this though by using an enum to bundle all your different Cells into a single type.

enum CellTypes {
case checkList(CheckListCell)
case segmented(SegmentedCell)
}

Now your array would be a homogeneous array of [CellTypes] where each element would be a case of the enum which would then contain the model of the cell inside it.

struct Submission {
let cells: [CellTypes]
}

This takes some custom decoding to get straight from JSON but I can't add that right now. If you need some guidance on that I'll update the answer.

Encoding and Decoding

Something to note from a JSON point of view. Your app will need to know which type of cell is being encoded/decoded. So your original JSON schema will need some updating to add this.

The automatic update from Firestore that you have shown is a fairly common way of doing this...

The JSON looks a bit like this...

{
"cells":
[
{
"checkListCell": {
"header": "dummy header"
}
},
{
"segmentedCell": {
"title": "dummy title"
}
}
]
}

Essentially, each item in the array is now an object that has a single key. From checkListCell, segmentedCell. This will be from any of the cases of your enum. This key tells your app which type of cell the object is.

Then the object shown against that key is then the underlying cell itself.

This is probably the cleanest way of modelling this data.

So, you might have two checklist cells and then a segmented cell and finally another checklist cell.

This will look like...

{
"cells":
[
{
"checkListCell": {
"header": "First checklist"
}
},
{
"checkListCell": {
"header": "Second checklist"
}
},
{
"segmentedCell": {
"title": "Some segmented stuff"
}
},
{
"checkListCell": {
"header": "Another checklist"
}
},
]
}

The important thing to think when analysing this JSON is not that it's harder for you (as a human being) to read. But that it's required, and actually fairly easy, for your app to read and decode/encode.

Hope that makes sense.

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 create a model in Swift from different data-type Json array?

That is some truly strange json but it can be decoded :)

First change MixedData to

struct MixedData: Codable {
let addressField: String
let someData: GetSomeData
}

and then add a custom init(from:) to RequestMyData where we use an unkeyed container for the input and output arrays and when looping those arrays we create another unkeyed container to decode to the MixedData type

init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
id = try container.decode(String.self, forKey: .id)
timeUsed = try container.decode(Int.self, forKey: .timeUsed)

input = try Self.decodeMixedData(try container.nestedUnkeyedContainer(forKey: .input))
output = try Self.decodeMixedData(try container.nestedUnkeyedContainer(forKey: .output))

getSomeData = try container.decode(GetSomeData.self, forKey: .getSomeData)
}

private static func decodeMixedData(_ unkeyedContainer: UnkeyedDecodingContainer) throws -> [MixedData] {
var container = unkeyedContainer
var result = [MixedData]()
while !container.isAtEnd {
var innerContainer = try container.nestedUnkeyedContainer()
let mixed = MixedData(addressField: try innerContainer.decode(String.self),
someData: try innerContainer.decode(GetSomeData.self))
result.append(mixed)
}

return result
}

Note that the property getSomeData is not an array.

Swift Codable: Decode different array of items with same root objects

My suggestion is to decode the dictionary/dictionaries for items separately

struct Item : Decodable {

enum CodingKeys: String, CodingKey {
case id = "timeEntryID"
case description, customerName, projectName
case startDateTime = "start"
case endDateTime = "end"
}

let id: Int
let startDateTime: Date
let endDateTime: Date
let customerName: String
let projectName: String
let description: String?

init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
id = Int(try container.decode(String.self, forKey: .id))!
description = try container.decodeIfPresent(String.self, forKey: .description)
customerName = try container.decode(String.self, forKey: .customerName)
projectName = try container.decode(String.self, forKey: .projectName)
startDateTime = Date(timeIntervalSince1970: Double(try container.decode(String.self, forKey: .startDateTime))!)
endDateTime = Date(timeIntervalSince1970: Double(try container.decode(String.self, forKey: .endDateTime))!)
}
}

And in Activity use a conditional initializer, it provides it's own do catch block. First it tries to decode a single item and assigns the single item as array to the property. If it fails it decodes an array.

enum KimaiAPIResponseKeys: String, CodingKey {
case result, id

enum KimaiResultKeys: String, CodingKey {
case success
case items
}
}

struct Activity: Decodable {
let id: String
let items: [Item]
}

extension Activity {

init(from decoder: Decoder) throws {
let rootContainer = try decoder.container(keyedBy: KimaiAPIResponseKeys.self)
id = try rootContainer.decode(String.self, forKey: .id)
let resultContainer = try rootContainer.nestedContainer(keyedBy: KimaiAPIResponseKeys.KimaiResultKeys.self, forKey: .result)
do {
let item = try resultContainer.decode(Item.self, forKey: .items)
items = [item]
} catch {
items = try resultContainer.decode([Item].self, forKey: .items)
}
}
}

Filter an (Codable) array by another array

You need to do that filter for each id in the favourites array. You get an array of arrays as a result. To get the final array, you need to join those arrays to a single array. This "map each thing to an array and join the arrays" operation is what a flatMap does:

workoutFav.flatMap { favouriteId in // for each favourite ID
jsonErgWorkouts.filter { $0.id == favouriteId } // find workouts that match the ID
} // flatMap joins all those arrays returns by "filter" together, no need to do anything else


Related Topics



Leave a reply



Submit