Swift Codable decode empty json as nil or empty object

17,879

Solution 1

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)

Solution 2

This is what your implementation of init(from: Decoder) should look like.

Note: You should consider changing LoginUserResponse from a class to a struct, since all it does is store values.

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

Solution 3

My recommendation is to decode result as enum and to initialize data on success.

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
}

Solution 4

This is because {} is an empty object but not nil. You have 2 options:

  1. change on server to return null instead of {} for data key
  2. implement custom initializer init(from: Decoder) and handle this case manually

Solution 5

Seems it's not possible to treat {} as null, so instead I've created a simple tool to "fix" the API response:

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.

Share:
17,879
Makalele
Author by

Makalele

Happy coder

Updated on June 15, 2022

Comments

  • Makalele
    Makalele about 2 years

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

    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.

    I'm using the same approach on Android using retrofit and it works fine. I rather don't want to make all fields as optional.

    Is there a way to make parser treat empty json {} as nil? Or make LoginUserResponseData as non-optional and it'll just have default values? 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.

  • Makalele
    Makalele over 6 years
    1. I cannot change server. It already works with Android app using retrofit and it parses it no problem. 2. As I said I don't want to do this, because I have the same situation in tons of queries. There must be a simple way.
  • Makalele
    Makalele over 6 years
    Isn't there some rule to treat {} as null? :)
  • Sam
    Sam over 6 years
    Nope @Makalele. What you could do instead is not make data an optional and make userId and name optional strings. This will help you omit the init(from decoder:) Should look like this: struct LoginUserResponseData : Codable { var userId: String? var name: String? }
  • Makalele
    Makalele over 6 years
    I know that, but this is simplified example. In reality I have more than 20 fields. I'm thinking about replacing string data:{} with data:null before calling the parser.
  • Mike Taverne
    Mike Taverne over 6 years
    This is hacking, not programming. You have no guarantee that your server won’t return those braces with a space between them, which is perfectly valid JSON.
  • Mike Taverne
    Mike Taverne over 6 years
    This is a neat approach, but could you go one step further and declare data as UserData? and set it to nil in the .error case? This seems preferable to a UserData object with fake values.
  • Makalele
    Makalele over 6 years
    I know that's why I wrote other guy's answer should be used. I only posted what I used, because of this very specific case. I just don't have time to write all decoders, because of one thing and I cannot change the server, because it may break already working android app.
  • Arsonik
    Arsonik over 5 years
    Yes but the LoginUserResponseData should fail because it cannot be decoded with an empty dictionary !
  • Arsonik
    Arsonik over 5 years
    We are not talking about a null value here ! but the fact that LoginUserResponseData init should fail. im gonna take it off in the example
  • Nata Mio
    Nata Mio over 4 years
    The answer gave a hint for handling empty 200 http code such as or empt string response to make it pass the decoder: if let httpResponse = response { if httpResponse.statusCode == 200 && data.isEmpty{ if let string = "{}".data(using: .utf8) { return Result { try decoder.decode(T.self, from: string) } } }else { print(context) } }else { print(context) }
  • promacuser
    promacuser over 3 years
    This will also silence legitimate errors. For example, if the upstream API changes the definition of one field, the object will return nil instead of throwing the Type Error.