Сглаживание JSON, когда ключи известны только во время выполнения

Допустим, у нас есть структура JSON, подобная следующей (обычно используемая в базе данных Firebase Realtime):

{
  "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"
  }
}

Ожидаемое решение:

С помощью Decodable Я хотел бы преобразовать его в массив из 3 элементов:

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

    init(from decoder: Decoder) throws {
        // ...
    }
}

let entities: [BoringEntity] = try! JSONDecoder()...

Атрибут id соответствует корневой строке объекта json, например: 18348b9b-9a49-4e04-ac35-37e38a8db1e2,

Временное решение:

Я уже попробовал несколько подходов, но не смог получить атрибут id, не требуя вспомогательной сущности (или используя дополнительные параметры):

/// Incomplete BoringEntity version to make Decodable conformance possible.
struct BoringEntityIncomplete: Decodable {
    let isActive: Bool
    let age: Int
    let company: String
}

// Decode to aux struct
let decoded = try! JSONDecoder().decode([String : BoringEntityIncomplete].self, for: jsonData)
// Map aux entities to BoringEntity
let entities = decoded.map { BoringEntity(...) }

С помощью init(from: Decoder) не так тривиально, как в других случаях, так как keyedContainer(,) не может быть использован из-за того, что ключ неизвестен.


Является Decodable не подходит для таких случаев?

2 ответа

Решение

Базовая сущность:

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

Решение 1. Использование дополнительной структуры без ключа

/// 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)

Решение 2. Использование обертки

Благодаря Code Different я смог объединить свой подход с его PhantomKeys идея, но нет никакого способа обойти это: всегда нужно использовать дополнительную сущность.

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)

Пара вещей, прежде чем я отвечу на ваш вопрос:

1: комментарий (// id) делает JSON недействительным. JSON не позволяет комментировать.

2: Где находится id недвижимость в BoringEntity родом из?

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

Если я пропущу эти вещи, вы можете обернуть массив BoringEntity в структуре (BoringEntities). С помощью [BoringEntity] напрямую не рекомендуется, так как вы должны затмить по умолчанию init(from decoder:) из Array,

Хитрость здесь в том, чтобы сделать JSONDecoder возвращает список ключей через container.allKeys имущество:

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)
        }
    }
}

Использование:

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
Другие вопросы по тегам