Skip to content

Instantly share code, notes, and snippets.

@LeeKahSeng
Last active June 4, 2023 14:04
Show Gist options
  • Save LeeKahSeng/ceb374a8f297e0b5982f926f6b43ea12 to your computer and use it in GitHub Desktop.
Save LeeKahSeng/ceb374a8f297e0b5982f926f6b43ea12 to your computer and use it in GitHub Desktop.
Decode and Flatten JSON with Dynamic Keys Using Swift Decodable (https://swiftsenpai.com/swift/decode-dynamic-keys-json/)
import Foundation
let jsonString = """
{
"S001": {
"firstName": "Tony",
"lastName": "Stark"
},
"S002": {
"firstName": "Peter",
"lastName": "Parker"
},
"S003": {
"firstName": "Bruce",
"lastName": "Wayne"
}
}
"""
struct Student: Decodable {
let firstName: String
let lastName: String
let studentId: String
enum CodingKeys: CodingKey {
case firstName
case lastName
}
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
// Decode firstName & lastName
firstName = try container.decode(String.self, forKey: CodingKeys.firstName)
lastName = try container.decode(String.self, forKey: CodingKeys.lastName)
// Extract studentId from coding path
studentId = container.codingPath.first!.stringValue
}
}
struct DecodedArray<T: Decodable>: Decodable {
typealias DecodedArrayType = [T]
private var array: DecodedArrayType
// Define DynamicCodingKeys type needed for creating decoding container from JSONDecoder
private struct DynamicCodingKeys: CodingKey {
// Use for string-keyed dictionary
var stringValue: String
init?(stringValue: String) {
self.stringValue = stringValue
}
// Use for integer-keyed dictionary
var intValue: Int?
init?(intValue: Int) {
// We are not using this, thus just return nil
return nil
}
}
init(from decoder: Decoder) throws {
// Create decoding container using DynamicCodingKeys
// The container will contain all the JSON first level key
let container = try decoder.container(keyedBy: DynamicCodingKeys.self)
var tempArray = DecodedArrayType()
// Loop through each keys in container
for key in container.allKeys {
// Decode T using key & keep decoded T object in tempArray
let decodedObject = try container.decode(T.self, forKey: DynamicCodingKeys(stringValue: key.stringValue)!)
tempArray.append(decodedObject)
}
// Finish decoding all T objects. Thus assign tempArray to array.
array = tempArray
}
}
// Transform DecodedArray into custom collection
extension DecodedArray: Collection {
// Required nested types, that tell Swift what our collection contains
typealias Index = DecodedArrayType.Index
typealias Element = DecodedArrayType.Element
// The upper and lower bounds of the collection, used in iterations
var startIndex: Index { return array.startIndex }
var endIndex: Index { return array.endIndex }
// Required subscript, based on a dictionary index
subscript(index: Index) -> Iterator.Element {
get { return array[index] }
}
// Method that returns the next index when iterating
func index(after i: Index) -> Index {
return array.index(after: i)
}
}
let jsonData = Data(jsonString.utf8)
// Decode JSON into [Student]
let decodedResult = try! JSONDecoder().decode(DecodedArray<Student>.self, from: jsonData)
@LeeKahSeng
Copy link
Author

let jsonString = """ { "students': { "S001": { "firstName": "Tony", "lastName": "Stark" }, "S002": { "firstName": "Peter", "lastName": "Parker" }, "S003": { "firstName": "Bruce", "lastName": "Wayne" } } }

any idea how to handle this structure??????

It is fairly straightforward, all you need to do is to create another data type to hold the student array. Here's how:

struct MyData: Decodable {
    let students: DecodedArray<Student>
}

let jsonData = Data(jsonString.utf8)
let decodedResult = try! JSONDecoder().decode(MyData.self, from: jsonData)
print(decodedResult.students)

@ColGren
Copy link

ColGren commented Jul 23, 2022

Does this method work if the Student Type also contains an nested element? .. I've been trying to decode a JSON response where this is the case using your excellent tutorial, and as soon as I try to decode a nested element it fails to decode.

for example.. I might have a json response of different Schools, containing Classrooms, Teachers and where the Student name is the identifier, and each Student has a nested list of Books they carry..

your method works up to the point i try to decode the nested Books.. (although I might well be making an error somewhere)

@LeeKahSeng
Copy link
Author

Does this method work if the Student Type also contains an nested element? .. I've been trying to decode a JSON response where this is the case using your excellent tutorial, and as soon as I try to decode a nested element it fails to decode.

for example.. I might have a json response of different Schools, containing Classrooms, Teachers and where the Student name is the identifier, and each Student has a nested list of Books they carry..

your method works up to the point i try to decode the nested Books.. (although I might well be making an error somewhere)

Assuming this is the JSON you trying to decode:

{
  "S001": {
    "firstName": "Tony",
    "lastName": "Stark",
    "books": [
      {
        "name": "book1"
      },
      {
        "name": "book2"
      },
      {
        "name": "book3"
      }
    ]
  },
  "S002": {
    "firstName": "Peter",
    "lastName": "Parker",
    "books": []
  },
  "S003": {
    "firstName": "Bruce",
    "lastName": "Wayne",
    "books": []
  }
}

All you need to do is to create a new Book struct and update the Student struct accordingly:

struct Book: Decodable {
    let name: String
}

struct Student: Decodable {

    let firstName: String
    let lastName: String
    let studentId: String
    let books: [Book]

    enum CodingKeys: CodingKey {
        case firstName
        case lastName
        case books
    }

    init(from decoder: Decoder) throws {

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

        // Decode firstName & lastName
        firstName = try container.decode(String.self, forKey: CodingKeys.firstName)
        lastName = try container.decode(String.self, forKey: CodingKeys.lastName)

        // Extract studentId from coding path
        studentId = container.codingPath.first!.stringValue
        
        // Decode books
        books = try container.decode([Book].self, forKey: CodingKeys.books)
    }
}

@cparker101
Copy link

cparker101 commented Oct 11, 2022 via email

@gmcusaro
Copy link

gmcusaro commented Jun 4, 2023

In extension DecodedArray: Collection the correct associated type for a collection's iterator element is Element, not Iterator.Element.

    // Required subscript, based on a dictionary index
    subscript(index: Index) -> Element {
        get { return array[index] }
    }

@phuongddx
Copy link

{
  "EventName": "broadcast-price",
  "EventData": {
    "data": {
      "XRPUSDT": {
        "change24h": 0.27,
        "change7d": 10.89,
        "exchange": "bsc",
        "icon": "",
        "id": "",
        "isDown": false,
        "isUp": true,
        "max24h": 0,
        "min24h": 0,
        "name": "",
        "pair": "",
        "price": 0.5245,
        "symbol": "XRPUSDT",
        "timeStamp": "2023-06-04T07:41:57.29489505Z",
        "volume_24h": 71163040.90439999,
        "volume_24h_origin": 0
      },
      "exchange": "bsc",
      "token": "XRPUSDT"
    },
    "event": "symbol-data"
  }
}

How about for single object sir? @LeeKahSeng Thanks!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment