7

In my quest to learn more about Swift, I'm looking at ways to improve my app and noticed a few places where I'm making assumptions where perhaps I shouldn't be.

When creating a new object, lets say a 'student', they need things like a name (String), age (Int) and score (Float). I read these from a JSON file, and put them into an object like this:

// note, details is a [String:Any] type
let name = details["name"] as! String
let age = details["age"] as! Int 
let score = Float(details["score"])
self.student = Student(name: name, tutor_group: tutor_group, score: score)

So my questions are as follows; 1. How should I modify my code to check that if a value is not a number, where it should be, the variable becomes just nil, or even better 0? 2. What if the key in the dictionary doesn't exist? 3. Are the different ways to do this, and if so, which is best practice?

Note that I want to keep this code as short as possible - if/else statements for each line are not what I'm looking for.

Thank you so much in advance!

4
  • When it comes to programming, on a general basis 'safe' and 'quick' do not mesh well together. Commented Sep 17, 2016 at 10:03
  • If you are reading the data from a file you are supposed to know if a key exists and what type the values are. In other words, check the contents of the file at compile time to be safe at runtime. By the way this makes the code as short as possible. Commented Sep 17, 2016 at 10:04
  • It depends a little upon how the JSON, itself, is formatted. Notable, these numeric values should not be strings, but rather should be numbers without quotes. Then NSJSONSerialization will do some of the conversion for you. E.g. you should have {"name": "Bob", "age": 29, "score": 29042.3}. Commented Sep 17, 2016 at 10:19
  • If an item that should be a number is not a number, then nil is exactly the right value, not 0. Commented Sep 17, 2016 at 12:16

3 Answers 3

9

The Solution suggested by the Swift team

Recently Apple described the suggested way to face this problem.

You can define the Student struct (or class) this way

struct Student {

    let name: String
    let age: Int
    let score: Float

    init?(json: [String:Any]) {
        guard
            let name = json["name"] as? String,
            let age = json["age"] as? Int,
            let score = json["score"] as? Float
            else { return nil }
        self.name = name
        self.age = age
        self.score = score
    }
}

Benefits of this approach

  1. You encapsulate the logic to convert a JSON into a Student inside the Student struct itself
  2. If the JSON doesn't contain a valid data (e.g. there is no age field with a valid Int) then the initializer fails and returns nil. This means there is no way to create a Student with missing fields (and this is a good thing) and this scenario will not cause a crash.

More

The post I linked above also describes a more advanced approach where a missing field/value throws an exception. However if you don't need to log (or notify to the caller) why the initialization failed you don't need this.

Sign up to request clarification or add additional context in comments.

4 Comments

I am really really thankful they created an official documentation for this. Creating model objects this way is the most sensible approach.
@Sulthan: I believed the official approach described by Apple is perfectly consistent with the Swift philosophy.
Of course it is but they never documented it so clearly. There are still many programmers that leave they data as JSONs all the time, implementing search operations over JSONs, modifying nested JSONs etc.
@Sulthan: Yes I agree :)
4

So my questions are as follows; 1. How should I modify my code to check that if a value is not a number, where it should be, the variable becomes just nil, or even better 0? 2. What if the key in the dictionary doesn't exist? 3. Are the different ways to do this, and if so, which is best practice?

let age = (details["age"] as? Int) ?? 0
  • In all cases, age will have the type Int
  • If the key doesn't exist, details["age"] will return nil, as? Int will return an Int? with value nil and the nil coalescing operator ?? will set the value to 0.
  • If the type isn't an Int, the conditional cast as? Int will return nil and the value will be set to 0.
  • In the expected case, age will have the Int value that was stored in details["age"].

For the other fields:

let name = (details["name"] as? String) ?? ""

// If score is stored as a String
let score = Float(details["score"] as? String ?? "") ?? 0

OR

// If score is stored as a number
let score = (details["score"] as? Float) ?? 0

Comments

3

You can use guard instead:

guard let name = details["name"] as? String else {
   return
}
print("\(name)")

Thanks!

2 Comments

You may not want to bail so early, instead it is possible that the OP wants to provide a default parameter value.
In that case, we can use Nil coalescing operator to assign any default value to the object. let name = (details["name"] as? String) ?? ""

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.