Swift Json декодирует вложенный массив / словарь в плоскую модель

Я пытаюсь декодировать следующий объект json для моей модели User в Swift.

Моя проблема заключается в декодировании значений _id и token из массива tokens, где первый токен в массиве содержит значения, которые я хочу декодировать в User.tokenId и User.token.

Я пытаюсь извлечь / отобразить значения непосредственно в структуру моей модели пользователя, не имея другой вложенной структуры в моей модели пользователя (например, struct Token { var id: String , var token: String })

let json = """
    {
        "currentLocation": {
            "latitude": 0,
            "longitude": 0
        },
        "profileImageUrl": "",
        "bio": "",
        "_id": "601453e4aae564fc19075b68",
        "username": "johnsmith",
        "name": "john",
        "email": "[email protected]",
        "keywords": ["word", "weds"],
        "tokens": [
            {
                "_id": "213453e4aae564fcqu775b69",
                "token": "eyJhbGciOiJIUzqoNiIsInR5cCI6IkpXVCJ9.eyJfaWQiOiI2MDE0NTNlNGFhZTU2NGZjMTkwNzViNjgiLCJpYXQiOjE2MTE5NDQ5MzIsImV4cCI6MTYxMjM3NjkzMn0.PbTsA3B0MAfcVvEF1UAMhUXFiqIL1FcxVFGgMTZ5HCk"
            }
        ],
        "createdAt": "2021-01-29T18:28:52.845Z",
        "updatedAt": "2021-01-29T18:28:52.883Z"
    }
    """.data(using: .utf8)!


struct User: Codable {
    var latitude: Double 
    var longitude: Double 
    var profileImageUrl: String 
    var bio: String 
    var userId: String 
    var username: String
    var name: String
    var email: String
    var keywords: [String]
    var tokenId: String
    var token: String
    var createdAt: Date
    var updatedAt: Date
    
    private enum UserKeys: String, CodingKey {
        case currentLocation
        case profileImageUrl
        case bio
        case userId = "_id"
        case username
        case name
        case email
        case keywords
        case tokens
        case createdAt
        case updatedAt
    }
    
    private enum CurrentLocationKeys: String, CodingKey { 
        case latitude
        case longitude
    }
    
    private enum TokenKeys: String, CodingKey {
        case tokenId = "_id"
        case token
    }
    
    init(from decoder: Decoder) throws {
        
        let userContainer = try decoder.container(keyedBy: UserKeys.self)
              let currentLocationContainer = try userContainer.nestedContainer(keyedBy: CurrentLocationKeys.self, forKey: .currentLocation) 
              self.latitude = try currentLocationContainer.decode(Double.self, forKey: .latitude)
              self.longitude = try currentLocationContainer.decode(Double.self, forKey: .longitude)
            self.profileImageUrl = try userContainer.decode(String.self, forKey: .profileImageUrl)
            self.bio = try userContainer.decode(String.self, forKey: .bio)
            self.userId = try userContainer.decode(String.self, forKey: .userId)
            self.username = try userContainer.decode(String.self, forKey: .username)
            self.name = try userContainer.decode(String.self, forKey: .name)
            self.email = try userContainer.decode(String.self, forKey: .email)
            self.keywords = try userContainer.decode([String].self, forKey: .keywords)
              let tokensContainer = try userContainer.nestedContainer(keyedBy: TokenKeys.self, forKey: .tokens)
              self.tokenId = try tokensContainer.decode(String.self, forKey: .tokenId)
              self.token = try tokensContainer.decode(String.self, forKey: .token)
            self.createdAt = try userContainer.decode(Date.self, forKey: .createdAt)
            self.updatedAt = try userContainer.decode(Date.self, forKey: .updatedAt)
    }
}

let user = try! decoder.decode(User.self, from: json)

person Community    schedule 29.01.2021    source источник
comment
Вы можете либо написать собственный код декодирования, либо (и это то, что я бы сделал) использовать вложенные структуры и предоставить вычисляемые свойства во внешней структуре, которые раскрывают вложенные значения.   -  person Paulw11    schedule 30.01.2021
comment
Показать ошибку, но tokens является массивом (это ошибка в консоли, если вы ее читаете), поэтому, если есть несколько значений, какое из них вы сохраните?   -  person Larme    schedule 30.01.2021
comment
Если что-то является массивом, для этого обычно есть причина. Вам понадобится стратегия для работы с 0 или более чем одним токеном, и, вероятно, будет лучше делать это за пределами вашего декодированного объекта. Я настоятельно рекомендую напрямую отображать объекты и заниматься особыми случаями, не связанными с декодированием. Это значительно уменьшит требуемый код, а также снизит когнитивную сложность.   -  person Patru    schedule 30.01.2021


Ответы (1)


Прежде всего, я предполагаю, что ваш decoder имеет подходящую стратегию декодирования даты, чтобы иметь возможность декодировать строки ISO8601 в Date.

Вмещающий контейнер словаря token - это массив. Вы должны вставить промежуточный nestedUnkeyedContainer

...
var arrayContainer = try userContainer.nestedUnkeyedContainer(forKey: .tokens)
let tokensContainer = try arrayContainer.nestedContainer(keyedBy: TokenKeys.self)
self.tokenId = try tokensContainer.decode(String.self, forKey: .tokenId)
self.token = try tokensContainer.decode(String.self, forKey: .token)
...

Гораздо меньше кода для декодирования JSON в несколько структур.

person vadian    schedule 29.01.2021
comment
Как уже говорилось, кода слишком много, чтобы избежать двух вложенных свойств. Пользователь может использовать lazy var, чтобы скрыть этот факт для методов более высокого уровня. - person Larme; 30.01.2021
comment
Спасибо, Вадиан! Да, у меня есть собственная стратегия декодирования даты, не включенная в этот код: - person ; 30.01.2021