1

normal case:

{
    "maintenance": true
}

{
    "maintenance": false
}

If there is no maintenance station then it will become empty string

{
    "maintenance": ""
}

i want to have nil if maintenance is empty string in json

struct Demo: Codable {
    var maintenance: Bool?
}

Is there a good way to do it?

6 Answers 6

4

In most cases, I'd probably use SeaSpell or Sh_Khan's solutions. Bool? is usually not the right type, but in this case it seems precisely what you mean (you seem to want to keep track of whether the value was set or not, rather than defaulting to something). But those approaches do require a custom decoder for the whole type which might be inconvenient. Another approach would be to define a new type just for this 3-way value:

enum Maintenance {
    case `true`, `false`, unset
}

(Maybe "enabled" and "disabled" would be better here than taking over the true and false keywords. But just showing what Swift allows.)

You can then decode this in a very strict way, checking for true, false, or "" and rejecting anything else ("false" for example).

extension Maintenance: Decodable {
    init(from decoder: Decoder) throws {
        let container = try decoder.singleValueContainer()
        if let value = try? container.decode(Bool.self) {
            self = value ? .true : .false
        } else if let value = try? container.decode(String.self), value.isEmpty {
            self = .unset
        } else {
            throw DecodingError.dataCorruptedError(in: container,
                                                   debugDescription: "Unable to decode maintenance")
        }
    }
}

Depending on how the rest of your code work, this three-way enum may be more convenient or less convenient than an Optional Bool, but it's another option.

With this, you don't need anything special in Demo:

struct Demo: Decodable {
    var maintenance: Maintenance
}

An important distinction is that maintenance here is not optional. It is required. There are just three acceptable values. Even if using Bool? you should think hard about whether there is a difference between "" and missing.

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

Comments

3

What you need is to try to decode your Bool, catch the error, try to decode a string and check if it is an empty string otherwise throw the error. This will make sure you don't discard any decoding error even if it is a string but not empty:


struct Demo: Codable {
    var maintenance: Bool?
}

struct Root: Codable {
    var maintenance: Bool?
    init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)
        do {
            maintenance = try container.decode(Bool.self, forKey: .maintenance)
        } catch {
            guard try container.decode(String.self, forKey: .maintenance) == "" else {
                throw error
            }
            maintenance = nil
        }
    }
}

Playground testing:

let json1 = """
{
    "maintenance": true
}
"""
let json2 = """
{
    "maintenance": false
}
"""
let json3 = """
{
    "maintenance": ""
}
"""
let json4 = """
{
    "maintenance": "false"
}
"""

do {
    let root1 = try JSONDecoder().decode(Root.self, from: Data(json1.utf8))
    print("root1", root1)
} catch {
    print(error)
}

do {
    let root2 = try JSONDecoder().decode(Root.self, from: Data(json2.utf8))
    print("root2", root2)
} catch {
    print(error)
}

do {
    let root3 = try JSONDecoder().decode(Root.self, from: Data(json3.utf8))
    print("root3", root3)
} catch {
    print(error)
}

do {
    let root4 = try JSONDecoder().decode(Root.self, from: Data(json4.utf8))
    print("root4", root4)
} catch {
    print(error)
}

This will print

root1 Root(maintenance: Optional(true))
root2 Root(maintenance: Optional(false))
root3 Root(maintenance: nil)
typeMismatch(Swift.Bool, Swift.DecodingError.Context(codingPath: [CodingKeys(stringValue: "maintenance", intValue: nil)], debugDescription: "Expected to decode Bool but found a string/data instead.", underlyingError: nil))

Comments

2

You can try

struct Root: Codable {
    var maintenance: Bool? 
    init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self) 
        do {
            self.maintenance = try container.decode(Bool.self, forKey: .maintenance) 
        }
        catch { 
        } 
    }
}

1 Comment

Why not try??
1

You really only have a couple of decent options that I know of.

  1. do codable init yourself and loose the free one.

    struct Root: Codable {
        var maintenance: Bool? 
    
        init(from decoder: Decoder) throws {
            let container = try decoder.container(keyedBy: CodingKeys.self) 
            let value = (try? container.decode(Bool.self, forKey: .maintenance) ?? "")
            self.maintenance = (value.isEmpty() || value == "false" ) ? nil : true
        }
    }
    

For obvious reasons this may not be ideal, especially if you have a lot of other variables to decode. The other option is to use a getter and add a variable to store the string optional.

  1. Calculated var

     private var _maintenance: String?
     var maintenance: Bool {
         get {
               ((_maintenance ?? "").isEmpty || _maintenance == "false") ? false : true
         }
     }
    

This solution is more ideal because you only need to change coding keys and add a var.

1 Comment

Notice I made the 2nd way calculate an actual bool. I personally don't use optional with booleans, although you may still want to in that case just return nil in getter and make the bool optional again..
0

Bool? is a very bad idea unless you know exactly what you are doing, because you have three possible values: true, false and nil.

You can’t use it directly in an if statement. You can compare b == true, b == false or b == nil and I expect that b == false will not produce the result you expect.

You want to use a three-value type for two possible values, which is asking for trouble.

1 Comment

In this case it seems that the OP wants the three-way value. "True, false, unset" is valid when you mean that. Optional Bools are a little weird, but exactly right when you want things like nested configurations ("this value or inherit from a higher layer").
0

I'd get the API changed because that's just bad design to have a field with two different types. It'd be much more sensible to have it as something like

maintenance: { active: true }

And for the case where there's no station, either set it as null or remove the field

1 Comment

I also think it is very poor design, but the api is provided by the government, I cannot change it.

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.