Home > database >  Decoding json with optional fields
Decoding json with optional fields

Time:06-11

I am trying to parse a json string in which some of the keys are not fixed. There are some keys which are related to error and either it will be error or the result data. Followings are the two examples:

{
    "ok": true,
    "result": {
        "code": "694kyH",
        "short_link": "shrtco.de\/694kyH",
        "full_short_link": "https:\/\/shrtco.de\/694kyH",
        "short_link2": "9qr.de\/694kyH",
        "full_short_link2": "https:\/\/9qr.de\/694kyH",
        "short_link3": "shiny.link\/694kyH",
        "full_short_link3": "https:\/\/shiny.link\/694kyH",
        "share_link": "shrtco.de\/share\/694kyH",
        "full_share_link": "https:\/\/shrtco.de\/share\/694kyH",
        "original_link": "http:\/\/google.com"
    }
}

{
    "ok": false,
    "error_code": 2,
    "error": "This is not a valid URL, for more infos see shrtco.de\/docs"
}

How will I parse this JSON. I have tried to build my class like following but it is not working:

struct ShortLinkData: Codable {
    let ok: Bool
    let result: Result?
    let errorCode: Int?
    let error: String?
    
    private enum CodingKeys : String, CodingKey { case ok, result, errorCode = "error_code", error }

       init(from decoder: Decoder) throws {
          let container = try decoder.container(keyedBy: CodingKeys.self)
          ok = try container.decode(Bool.self, forKey: .ok)
           result = try container.decode(Result.self, forKey: .result)
           errorCode = try container.decodeIfPresent(Int.self, forKey: .errorCode)
           error = try container.decodeIfPresent(String.self, forKey: .error)
      }
}

// MARK: - Result
struct Result: Codable {
    let code, shortLink: String
    let fullShortLink: String
    let shortLink2: String
    let fullShortLink2: String
    let shortLink3: String
    let fullShortLink3: String
    let shareLink: String
    let fullShareLink: String
    let originalLink: String

    enum CodingKeys: String, CodingKey {
        case code
        case shortLink = "short_link"
        case fullShortLink = "full_short_link"
        case shortLink2 = "short_link2"
        case fullShortLink2 = "full_short_link2"
        case shortLink3 = "short_link3"
        case fullShortLink3 = "full_short_link3"
        case shareLink = "share_link"
        case fullShareLink = "full_share_link"
        case originalLink = "original_link"
    }
    init(from decoder: Decoder) throws {
       let container = try decoder.container(keyedBy: CodingKeys.self)
        code = try container.decode(String.self, forKey: .code)
        shortLink = try container.decode(String.self, forKey: .shortLink)
        fullShortLink = try container.decode(String.self, forKey: .fullShortLink)
        shortLink2 = try container.decode(String.self, forKey: .shortLink2)
        fullShortLink2 = try container.decode(String.self, forKey: .fullShortLink2)
        shortLink3 = try container.decode(String.self, forKey: .shortLink3)
        fullShortLink3 = try container.decode(String.self, forKey: .fullShortLink3)
        shareLink = try container.decode(String.self, forKey: .shareLink)
        fullShareLink = try container.decode(String.self, forKey: .fullShareLink)
        originalLink = try container.decode(String.self, forKey: .originalLink)
   }
}

My parsing code:

let str = String(decoding: data, as: UTF8.self)
print(str)
let shortURL = try? JSONDecoder().decode(ShortLinkData.self, from: data)
return shortURL!

I am always getting nil in shortURL object.

CodePudding user response:

You should split this into several steps in order to avoid to handle all these optionals in your model.

First create a struct that has only those properties that are guaranteed to be there. ok in your case:

struct OKResult: Codable{
    let ok: Bool
}

then create one for your error state and one for your success state:

struct ErrorResult: Codable{
    let ok: Bool
    let errorCode: Int
    let error: String
    
    private enum CodingKeys: String, CodingKey{
        case ok, errorCode = "error_code", error
    }
}

struct ShortLinkData: Codable {
    let ok: Bool
    let result: Result
}

struct Result: Codable {
    let code, shortLink: String
    let fullShortLink: String
    let shortLink2: String
    let fullShortLink2: String
    let shortLink3: String
    let fullShortLink3: String
    let shareLink: String
    let fullShareLink: String
    let originalLink: String

    enum CodingKeys: String, CodingKey {
        case code
        case shortLink = "short_link"
        case fullShortLink = "full_short_link"
        case shortLink2 = "short_link2"
        case fullShortLink2 = "full_short_link2"
        case shortLink3 = "short_link3"
        case fullShortLink3 = "full_short_link3"
        case shareLink = "share_link"
        case fullShareLink = "full_share_link"
        case originalLink = "original_link"
    }
}

Then you can decode the data:

guard try JSONDecoder().decode(OKResult.self, from: data).ok else{
    let errorResponse = try JSONDecoder().decode(ErrorResult.self, from: data)
    //handle error scenario
    fatalError(errorResponse.error) // or throw custom error or return nil etc...
}

let shortlinkData = try JSONDecoder().decode(ShortLinkData.self, from: data)

Remarks:

  • Your inits are not necessary.
  • Never use try? this will hide all errors from you
  • you would need to wrap this either in a do catch block ore make your function throwing and handle errors further up the tree.

CodePudding user response:

Actually there are no optional fields. The server sends two different but distinct JSON strings.

A suitable way to decode both JSON strings is an enum with associated values. It decodes the ok key, then it decodes either the result dictionary or errorCode and error

enum Response : Decodable {
    case success(ShortLinkData), failure(Int, String)
    
    private enum CodingKeys : String, CodingKey { case ok, result, errorCode, error }
    
    init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)
        let ok = try container.decode(Bool.self, forKey: .ok)
        if ok {
            let result = try container.decode(ShortLinkData.self, forKey: .result)
            self = .success(result)
        } else {
            let errorCode = try container.decode(Int.self, forKey: .errorCode)
            let error = try container.decode(String.self, forKey: .error)
            self = .failure(errorCode, error)
        }
    }
}

In ShortLinkData the init method and the CodingKeys are redundant if you specify the convertFromSnakeCase key decoding strategy

struct ShortLinkData: Decodable {
    let code, shortLink: String
    let fullShortLink: String
    let shortLink2, fullShortLink2: String
    let shortLink3, fullShortLink3: String
    let shareLink, fullShareLink: String
    let originalLink: String
}


do {
    let decoder = JSONDecoder()
    decoder.keyDecodingStrategy = .convertFromSnakeCase
    let result = try decoder.decode(Response.self, from: data)
    switch result {
        case .success(let linkData): print(linkData)
        case .failure(let code, let message): print("An error occurred with code \(code) and message \(message)")
    }
} catch {
    print(error)
}
  • Related