0

I am using Swift 4 and JSONDecoder. I have the following structure:

struct Customer: Codable {
    var id: Int!
    var cnum: String!
    var cname: String!
}

Note: the fields cannot be made optional.

Now I have a JSON string:

[
    {
        "id": 1,
        "cnum": "200",
        "cname": "Bob Smith"
    },
    {
        "id": 2,
        "cnum": "201",
        "cname": null
    }
]

And to decode it, I use the following:

let decoder = JSONDecoder()
let customers = try decoder.decode([Customer].self, from: json)

Everything works fine except the null data gets converted to nil. My question is, what would be the easiest way to convert incoming nil to an empty string ("")?

I would like to do this with the minimum amount of code but I'm not sure about the correct approach and at what point can the nil be converted to an empty string. Thank you beforehand.

Tamás Sengel
  • 55,884
  • 29
  • 169
  • 223
Robert Smith
  • 634
  • 1
  • 8
  • 22
  • 1
    If you don't want optionals why do you declare all variables as optional?? And **NEVER** declare properties as implicit unwrapped optional which are initialized with an `init` method anyway. – vadian Feb 01 '18 at 13:52
  • Vadian thank you and sorry for the confusion. When I meant optional, I mean it cannot be: var cname: String? – Robert Smith Feb 01 '18 at 14:07
  • 1
    If it could be `nil` declare it as *regular* optional `var cname: String?`. If not, as non-optional: `var cname: String` – vadian Feb 01 '18 at 14:09
  • Thank you Vadian, I had also tried that but that again would break objective-c calls. – Robert Smith Feb 01 '18 at 14:12

5 Answers5

5

You can use backing ivars:

struct Customer: Codable {
    var id: Int
    var cnum: String {
        get { _cnum ?? "" }
        set { _cnum = newValue }
    }
    var cname: String {
        get { _cname ?? "" }
        set { _cname = newValue }
    }
    
    private var _cnum: String?
    private var _cname: String?
    private enum CodingKeys: String, CodingKey {
        case id, _cnum = "cnum", _cname = "cname"
    }
}

Due to the custom CodingKeys, the JSON decoder will actually decode to _cnum and _cname, which are optional strings. We convert nil to empty string in the property getters.

Konstantin Nikolskii
  • 1,075
  • 1
  • 12
  • 17
Code Different
  • 90,614
  • 16
  • 144
  • 163
3

You can use decodeIfPresent method.

struct Source : Codable {

    let id : String?


    enum CodingKeys: String, CodingKey {
        case id = "id"

    }

    init(from decoder: Decoder) throws {
        let values = try decoder.container(keyedBy: CodingKeys.self)
        id = try values.decodeIfPresent(String.self, forKey: .id) ?? "Default value pass"

    }
}
krjw
  • 4,070
  • 1
  • 24
  • 49
1

You should make a computed variable that will have the original value if it's not nil and an empty string if it is.

var cnameNotNil: String {
    return cname ?? ""
}
Tamás Sengel
  • 55,884
  • 29
  • 169
  • 223
  • 1
    the4kman thanks for the editing! Thanks for the answer also but, I need the name to remain as cname though! That would create an additional variable. This would also break alot of other code. Thanks. – Robert Smith Feb 01 '18 at 13:51
  • Awesome update. I just tried it and for some reason didSet is not running? (placed breakpoint and code never ran inside didSet) Did you test it using JSONDecoder? – Robert Smith Feb 01 '18 at 13:56
  • @RobertSmith I reverted my answer to the previous state because it did not work. If you want to use `cname` for the name, provide a different coding key for the original variable and rename it to something like `cnameOriginal`. – Tamás Sengel Feb 01 '18 at 14:03
0

The usual way is to write an initializer which handles the custom behavior. The null value is caught in an extra do - catch block.

struct Customer: Codable {
    var id: Int
    var cnum: String
    var cname: String

    init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)
        id = try container.decode(Int.self, forKey: .id)
        cnum = try container.decode(String.self, forKey: .cnum)
        do { cname = try container.decode(String.self, forKey: .cname) }
        catch { cname = "" }
    }
}
vadian
  • 274,689
  • 30
  • 353
  • 361
  • Vadian, thank you and I did try that. But unfortunately when I put in an init, it breaks my calls to objective-c code. The closest answer was something in the lines of "didSet" but that would not work neither. – Robert Smith Feb 01 '18 at 14:08
  • 2
    You should have mentioned in the question how the code interacts with Objective-C. – vadian Feb 01 '18 at 14:10
0

Use decodeIfPresent if the value from response might be null

cnum = try container.decode(String.self, forKey: .cnum)
cname = try container.decodeIfPresent(String.self, forKey: .cname)
Keshav
  • 2,965
  • 3
  • 25
  • 30