Flattening JSON when keys are known only at runtime

后端 未结 2 1960
予麋鹿
予麋鹿 2020-12-18 17:11

Let\'s say we have a JSON structure like the following (commonly used in Firebase\'s Realtime Database):

{
  \"18348b9b-9a49-4e04-ac35-37e38a8db1e2\": {
             


        
相关标签:
2条回答
  • 2020-12-18 17:40

    A couple things before I answer your question:

    1: The comment (// id) makes the JSON invalid. JSON does not allow comments.

    2: Where does the id property in BoringEntity come from?

    struct BoringEntity: Decodable {
        let id: String          // where is it stored in the JSON???
        let isActive: Bool
        let age: Int
        let company: String
    }
    

    If I overlook these things, you can wrap the array of BoringEntity in a struct (BoringEntities). Using [BoringEntity] directly is not advisable since you have to overshadow the default init(from decoder:) of Array.

    The trick here is to make JSONDecoder gives you back the list of keys via the container.allKeys property:

    struct BoringEntity: Decodable {
        let isActive: Bool
        let age: Int
        let company: String
    }
    
    struct BoringEntities: Decodable {
        var entities = [BoringEntity]()
    
        // This really is just a stand-in to make the compiler happy.
        // It doesn't actually do anything.
        private struct PhantomKeys: CodingKey {
            var intValue: Int?
            var stringValue: String 
            init?(intValue: Int) { self.intValue = intValue; self.stringValue = "\(intValue)" }
            init?(stringValue: String) { self.stringValue = stringValue }
        }
    
        init(from decoder: Decoder) throws {
            let container = try decoder.container(keyedBy: PhantomKeys.self)
    
            for key in container.allKeys {
                let entity = try container.decode(BoringEntity.self, forKey: key)
                entities.append(entity)
            }
        }
    }
    

    Usage:

    let jsonData = """
    {
      "18348b9b-9a49-4e04-ac35-37e38a8db1e2": {
        "isActive": false,
        "age": 29,
        "company": "BALOOBA"
      },
      "20aca96e-663a-493c-8e9b-cb7b8272f817": {
        "isActive": false,
        "age": 39,
        "company": "QUONATA"
      },
      "bd0c389b-2736-481a-9cf0-170600d36b6d": {
        "isActive": false,
        "age": 35,
        "company": "EARTHMARK"
      }
    }
    """.data(using: .utf8)!
    
    let entities = try JSONDecoder().decode(BoringEntities.self, from: jsonData).entities
    
    0 讨论(0)
  • 2020-12-18 17:47

    Base entity:

    struct BoringEntity: Decodable {
        let id: String
        let isActive: Bool
        let age: Int
        let company: String
    }
    

    Solution 1: Using an extra struct without the key

    /// Incomplete BoringEntity version to make Decodable conformance possible.
    private struct BoringEntityBare: Decodable {
        let isActive: Bool
        let age: Int
        let company: String
    }
    
    // Decode to aux struct
    private let decoded = try! JSONDecoder().decode([String : BoringEntityBare].self, from: jsonData)
    // Map aux entities to BoringEntity
    let entities = decoded.map { BoringEntity(id: $0.key, isActive: $0.value.isActive, age: $0.value.age, company: $0.value.company) }
    print(entities)
    

    Solution 2: Using a wrapper

    Thanks to Code Different I was able to combine my approach with his PhantomKeys idea, but there's no way around it: an extra entity must always be used.

    struct BoringEntities: Decodable {
        var entities = [BoringEntity]()
    
        // This really is just a stand-in to make the compiler happy.
        // It doesn't actually do anything.
        private struct PhantomKeys: CodingKey {
            var intValue: Int?
            var stringValue: String
            init?(intValue: Int) { self.intValue = intValue; self.stringValue = "\(intValue)" }
            init?(stringValue: String) { self.stringValue = stringValue }
        }
    
        private enum BareKeys: String, CodingKey {
            case isActive, age, company
        }
    
        init(from decoder: Decoder) throws {
            let container = try decoder.container(keyedBy: PhantomKeys.self)
    
            // There's only one key
            for key in container.allKeys {
                let aux = try container.nestedContainer(keyedBy: BareKeys.self, forKey: key)
    
                let age = try aux.decode(Int.self, forKey: .age)
                let company = try aux.decode(String.self, forKey: .company)
                let isActive = try aux.decode(Bool.self, forKey: .isActive)
    
                let entity = BoringEntity(id: key.stringValue, isActive: isActive, age: age, company: company)
                entities.append(entity)
            }
        }
    }
    
    let entities = try JSONDecoder().decode(BoringEntities.self, from: jsonData).entities
    print(entities)
    
    0 讨论(0)
提交回复
热议问题