简体   繁体   English

Swift Codable 将空 json 解码为 nil 或空对象

[英]Swift Codable decode empty json as nil or empty object

Here's my code:这是我的代码:

class LoginUserResponse : Codable {
    var result: String = ""
    var data: LoginUserResponseData?
    var mess: [String] = []
}

public class LoginUserResponseData : Codable {
    var userId = "0"
    var name = ""
}

Now, calling the server API I'm parsing response like this (using Stuff library to simplify parsing):现在,调用服务器 API 我正在解析这样的响应(使用 Stuff 库来简化解析):

do {
    let loginUserResponse = try LoginUserResponse(json: string)
} catch let error {
    print(error)
}

When I enter the correct password I'm getting an answer like this:当我输入正确的密码时,我会得到这样的答案:

{"result":"success","data":{"userId":"10","name":"Foo"},"mess":["You're logged in"]}

This is fine, the parser is working correctly.这很好,解析器工作正常。

While providing wrong password gives the following answer:提供错误密码时会给出以下答案:

{"result":"error","data":{},"mess":["Wrong password"]}

In this situation, the parser is failing.在这种情况下,解析器失败。 It should set data to nil, but instead, it tries to decode it to the LoginUserResponseData object.它应该将 data 设置为 nil,但相反,它会尝试将其解码为 LoginUserResponseData 对象。

I'm using the same approach on Android using retrofit and it works fine.我在使用改造的 Android 上使用相同的方法,它工作正常。 I rather don't want to make all fields as optional.我宁愿不想将所有字段都设为可选。

Is there a way to make parser treat empty json {} as nil?有没有办法让解析器将空 json {} 视为 nil? Or make LoginUserResponseData as non-optional and it'll just have default values?或者将 LoginUserResponseData 设置为非可选的,它只有默认值? I know I can create a custom parser for this, but I have tons of requests like this and it'll require too much additional work.我知道我可以为此创建一个自定义解析器,但是我有大量这样的请求,并且需要太多额外的工作。

As easy as that !就这么简单!

class LoginUserResponse : Codable {
    var result: String = ""
    var data: LoginUserResponseData?
    var mess: [String] = []

    private enum CodingKeys: String, CodingKey {
        case result, data, mess
    }

    required init(from decoder: Decoder) throws {
        let values = try decoder.container(keyedBy: CodingKeys.self)
        result = try values.decode(String.self, forKey: .result)
        mess = try values.decode([String].self, forKey: .mess)
        data = try? values.decode(LoginUserResponseData.self, forKey: .data)
    }
}

public class LoginUserResponseData : Codable {
    var userId = "0"
    var name = ""
}

let str = "{\"result\":\"success\",\"data\":{\"userId\":\"10\",\"name\":\"Foo\"},\"mess\":[\"You're logged in\"]}"
let str2 = "{\"result\":\"error\",\"data\":{},\"mess\":[\"Wrong password\"]}"

let decoder = JSONDecoder()
let result = try? decoder.decode(LoginUserResponse.self, from: str.data(using: .utf8)!)
let result2 = try? decoder.decode(LoginUserResponse.self, from: str2.data(using: .utf8)!)
dump(result)
dump(result2)

My recommendation is to decode result as enum and to initialize data on success.我的建议是将result解码为enum并在成功时初始化data

struct LoginUserResponse : Decodable {

    enum Status : String, Decodable { case success, error }
    private enum CodingKeys: String, CodingKey { case result, data, mess }

    let result : Status
    let data : UserData?
    let mess : [String]

    init(from decoder: Decoder) throws
    {
        let values = try decoder.container(keyedBy: CodingKeys.self)
        result = try values.decode(Status.self, forKey: .result)
        mess = try values.decode([String].self, forKey: .mess)
        switch result {
            case .success: data = try values.decode(UserData.self, forKey: .data)
            case .error: data = nil
        }
    }
}

public struct UserData : Decodable {
    let userId : String
    let name : String
}

This is what your implementation of init(from: Decoder) should look like.这就是您的init(from: Decoder)实现应该是什么样子。

Note: You should consider changing LoginUserResponse from a class to a struct, since all it does is store values.注意:您应该考虑将LoginUserResponse从类更改为结构,因为它所做的只是存储值。

struct LoginUserResponse: Codable {
    var result: String
    var data: LoginUserResponseData?
    var mess: [String]

    init(from decoder: Decoder) throws
    {
        let values = try decoder.container(keyedBy: CodingKeys.self)
        result = try values.decode(String.self, forKey: .result)
        mess = try values.decode([String].self, forKey: .mess)
        if let d = try? values.decode(LoginUserResponseData.self, forKey: .data) {
            data = d
        }
    }
}

This is because {} is an empty object but not nil.这是因为{}是一个空对象,但不是 nil。 You have 2 options:您有 2 个选项:

  1. change on server to return null instead of {} for data key在服务器上更改以返回null而不是{}作为data
  2. implement custom initializer init(from: Decoder) and handle this case manually实现自定义初始化程序init(from: Decoder)并手动处理这种情况

Seems it's not possible to treat {} as null, so instead I've created a simple tool to "fix" the API response:似乎不可能将 {} 视为 null,因此我创建了一个简单的工具来“修复”API 响应:

extension String {

    func replaceEmptyJsonWithNull() -> String {
        return replacingOccurrences(of: "{}", with: "null")
    }

}

Other ways are described by @Vitaly Gozhenko and should be used, but I cannot change the server API nor want to write full custom decoder, because this one case. @Vitaly Gozhenko 描述了其他方法并且应该使用,但我不能更改服务器 API,也不想编写完整的自定义解码器,因为这是一种情况。

Wow, okay this doesn't work at all.哇,好吧,这根本不起作用。 Sorry.对不起。

I came across this post a few years late, but there are certain problems with each of the solutions.几年后我才看到这篇文章,但每个解决方案都存在某些问题。 Changing the JSON is potentially impractical, silencing the error with try?更改 JSON 可能不切实际, try? has the potential to ignore other, potentially legitimate errors.有可能忽略其他潜在的合法错误。

Here's a proposed solution that I have used in a project via extending KeyedDecodingContainer : ``` fileprivate extension KeyedDecodingContainer { private struct EmptyObject: Decodable {}这是我通过扩展KeyedDecodingContainer在项目中使用的建议解决方案:``` fileprivate extension KeyedDecodingContainer { private struct EmptyObject: Decodable {}

 func decodePossibleEmptyObject<T: Decodable>(_ key: K) throws -> T? { if let _ = try? decode(EmptyObject.self, forKey: key) { return nil } return try self.decode(T.self, forKey: key) } } ```

Creating an EmptyObject representation allows try?创建一个EmptyObject表示允许try? to only succeed if, in fact, the object is an empty object.只有当对象实际上是一个空对象时才会成功。 Otherwise, the decoder will continue to decode the object as requested, with errors falling through the method.否则,解码器将继续按请求解码对象,错误会落在该方法中。

The biggest downside is that this requires a custom init(from: Coder) .最大的缺点是这需要自定义init(from: Coder)

First time I was facing this, normally backend would send nil but I was receiving empty data.我第一次遇到这个问题时,通常后端会发送 nil 但我收到的是空数据。 Just make the data inside User data optional and it will work out of the box.只需将用户数据中的数据设为可选,它就会开箱即用。

Looks like tedious to be unwrapping when needed, but if you have your API Layer, and your Business Model Layer which you would build from your API object with the exact data that you need is totally fine.在需要时展开看起来很乏味,但如果您有 API 层,以及您将从 API 对象构建的业务模型层以及您需要的确切数据,那就完全没问题了。

struct LoginUserResponse : Codable {
    let result: String
    let data: LoginUserResponseData?
    let mess: [String] = []
}

struct LoginUserResponseData : Codable {
    let userId: String?
    let name: String?
}

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM