0

My Swift 5 (Xcode 11.5) app saves people in a text file (json format).

The structs I use to decode the json text:

struct People:Codable {
    var groupName:String
    var groupLocation:String 
    var members:[Person]

    init(_ gn:String,_ gl:String,_ m:[Person] {
        groupName = gn
        groupLocation = gl
        members = m
}

struct Person:Codable {
    var name:String
    var age:Int
    var profession:String?

    init(_ n:String,_ a:Int,_ p:String?) {
        name = n
        age = a
        profession = (p==nil) ? "none" : p!
    }
}

Decoding the contents of the text file:

let jsonData = Data(text.utf8)
let decoder = JSONDecoder()
let people = try decoder.decode(People.self, from: jsonData)
let members = people.members

for (i,member) in members.enumerated() {
    //print(member.profession==nil)
    //Do stuff
}

profession was added later on and there also might be more values added in the future but I want my app to be backwards compatible to older files. If profession doesn't exist, it should use "none" instead (see code above) but when I check member.profession after decoding, it's still nil. name, age,... all contain the right values, so that part works.

How do I give profession a value in the struct if it doesn't exist in the json file? What's the simplest/cleanest way to do this, so I can also add to it later on, if necessary (e.g. birthday, gender,...)?

Neph
  • 1,823
  • 2
  • 31
  • 69
  • 2
    Override `init(decoder:)`, and check the `decodeifPresent()` – Larme Sep 16 '20 at 10:14
  • 1
    Does this answer your question? [Swift Codable Decode Manually Optional Variable](https://stackoverflow.com/questions/46728324/swift-codable-decode-manually-optional-variable) – Larme Sep 16 '20 at 10:15
  • 5
    Why would use the string `"none"` to indicate a non-existent profession? `nil` is made for that exact purpose! – Sweeper Sep 16 '20 at 10:15
  • @Sweeper That's just an example. I need the String to have a value !=nil that's then displayed in my app without further checking. – Neph Sep 16 '20 at 10:17
  • @Larme I saw that question before I posted this one but honestly, I have no idea how that would work because not only do I not use enums but the two code paragraphs I posted above are also in 2 separate swift files. Plus, I don't just want to check if the `profession` exists in the json file, I also want to give it a custom value if it doesn't. – Neph Sep 16 '20 at 10:23
  • https://stackoverflow.com/a/46728424/1801544 use `decodeIfPresent()`. If you don't know where to put it in `init(from decoder: Decoder) throws`. Look for "Swift Codable Custom init". – Larme Sep 16 '20 at 10:25
  • @Larme Like I said, I don't use enums/CodingKeys. How do I give the decoded `profession` (which is nil if it doesn't exist) a value? – Neph Sep 16 '20 at 10:28
  • They are automatically generated! How do you think it match the names? See https://pastebin.com/VXR1sE1Z – Larme Sep 16 '20 at 10:33
  • @Larme The `CodingKey`s are? In the other question they're added by hand and the op also gave them custom values, which I want to do but only if a certain one is `nil`. – Neph Sep 16 '20 at 10:40
  • You can't use Coding Keys for that. May I suggest you learn more about Codable and CodingKey by reading [this article](https://developer.apple.com/documentation/foundation/archives_and_serialization/encoding_and_decoding_custom_types) from Apple – Joakim Danielson Sep 16 '20 at 10:56
  • @Larme Thanks for the pastebin. I just tested it and it works but is there a way to do this without the extra `extension`, so it's a bit "cleaner"? I tried to use the new `init` in the original struct but since I use the same `Person` to save the file again, Xcode gives me an error: `Extra arguments at positions #1, #2, #3 in call` (I guess it only expects a `Decoder` at that point). – Neph Sep 16 '20 at 11:25
  • Put the method inside the struct declaration, no need to the extension. – Larme Sep 16 '20 at 11:27
  • @Larme I tried to do that but I use the same `Person` to encode it and write everything to a file again and at the point where I create the new `Person` instances for that, Xcode complains: `Extra arguments at positions #1, #2, #3 in call`. – Neph Sep 16 '20 at 11:35
  • I don't think the OP even know what he's doing lol :) He want things to be automatically but it have to be pretty as he wanted. – Tj3n Sep 16 '20 at 11:44
  • @JoakimDanielson Thanks for the link. Unfortunately most of it is about `CodingKeys` and "manually" decoding, which Larme already suggested. Is there really no way to have it decode/encode everything automatically by providing a struct with the right structure but also be able to set a default value/change certain values, if necessary? Because that's not part of Apple's documentation. – Neph Sep 16 '20 at 11:46
  • @Tj3n I don't know how to do this, that's why I asked. ;) There has to be some way to change/check the value of `profession` after it's decoded automatically without making a huge "mess" of the code. – Neph Sep 16 '20 at 11:48
  • Well that's what everyone in here has been trying to tell you :) there's no shortcut. – Tj3n Sep 16 '20 at 12:01
  • @Tj3n So absolutely no way to change the values of what it decoded automatically? What did I do in my original code then with `profession = (p==nil) ? "none" : p!`? Is that init not being called automatically when it tries to decode the text? – Neph Sep 16 '20 at 12:31
  • @Larme Nevermind, the special `init` does work if it's in the struct BUT to also create the objects for the encoding the other init (the code I posted above but without `optional`) is needed too. Is `self` (for `name` and `age`) really necessary though? It works fine without. Something new I learned today: No need to create an `init` for a `struct ` if you want to set all of the properties with it. Apparently Swift creates an invisible one that you can use with the argument labels. Want to post your pastebin code as answer too? I think you posted it a couple of minutes before PGDev did. – Neph Sep 16 '20 at 13:00

1 Answers1

2

If you need to parse your JSON in some way other than what Codable already does, you need to implement the custom initializer init(from:), i.e.

struct Person:Codable {
    var name:String
    var age:Int
    var profession: String
    
    init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)
        self.name = try container.decode(String.self, forKey: .name)
        self.age = try container.decode(Int.self, forKey: .age)
        if let profession = try container.decodeIfPresent(String.self, forKey: .profession) {
            self.profession = profession
        } else {
            self.profession = "none"
        }
    }
}

Also, since you're always giving a value to profession, no need to make it optional.

PGDev
  • 23,751
  • 6
  • 34
  • 88
  • Larme linked a pastebin with this code, with the only difference that he added `extension Person` with the `init` inside. I first tried it without and put the new `init` in the original struct but Xcode gives me an error when I want to save the changes in the file again, using the same `Person`: `Extra arguments at positions #1, #2, #3 in call`. – Neph Sep 16 '20 at 11:21
  • Nevermind, this does work if it's in the struct BUT to also create the objects for the encoding the other init (the code I posted above but without `optional`) is needed too. Is `self` (for `name` and `age`) really necessary though? It works fine without. – Neph Sep 16 '20 at 12:53
  • Well using `self` with the properties is just a coding style that one follow. You can write without it whatever you're comfortable with. – PGDev Sep 16 '20 at 12:56
  • 1
    I see. I know that you do need the `self` for `profession` though, so it'll use the right one. – Neph Sep 16 '20 at 13:02