ZhgChg.Li

Codable Decode Issues Explained|Handling Null Response Fields Without Rewriting init Decoder

Developers facing Codable decode errors can resolve null response field issues efficiently without rewriting init decoder methods, improving data parsing reliability and reducing code complexity.

Codable Decode Issues Explained|Handling Null Response Fields Without Rewriting init Decoder

Real-world Codable Decode Issues Summary (Part 2)

Independent writing, free to read — please support these ads

 

Advertise here →

Handling Response Null Fields Properly Doesn’t Always Require Overriding init Decoder

Photo by Zan

Photo by Zan

Introduction

Following the previous article “Summary of Real-World Codable Decode Issues,” development has progressed and new scenarios and problems have emerged. This sequel continues to document the encountered situations and research process for future reference.

The previous article mainly solved the Decodable mapping from JSON String to Entity Object. Once we have the Entity Object, we can convert it into a Model Object for use within the program, a View Model Object to handle data display logic, and so on; on the other hand, we need to convert the Entity into an NSManagedObject to store it locally in Core Data.

Main Issue

Assuming our Song Entity structure is as follows:

struct Song: Decodable {
    var id: Int
    var name: String?
    var file: String?
    var converImage: String?
    var likeCount: Int?
    var like: Bool?
    var length: Int?
}

Since the API endpoint does not always return complete data fields (only the id is guaranteed), all fields except for id are Optional; for example, when fetching song information, a complete structure is returned, but when liking or favoriting a song, only the three related fields id, likeCount, and like are returned.

We want to store all available fields from the API response into Core Data, updating only the changed fields if the data already exists (incremental update).

But here comes the problem: after decoding Codable into an Entity Object, we cannot distinguish between “the data field is intended to be set to nil” and “the response did not provide the field”

A Response:
{
  "id": 1,
  "file": null
}
B Response:
{
  "id": 1,
  "like": true,
  "likeCount": 1
}

For the files in A Response and B Response, both are null, but their meanings are different; A intends to set the file field to null (clearing the original data), while B wants to update other data and simply does not provide the file field.

Some developers in the Swift community have proposed adding a null Strategy similar to date Strategy in JSONDecoder, allowing us to distinguish the above cases, but there are currently no plans to include it.

Solution

As mentioned before, our architecture is JSON String -> Entity Object -> NSManagedObject, so when we get the Entity Object, it is already the result after decoding, and there is no raw data to manipulate. Of course, we could compare and operate on the original JSON String here, but if we do that, it’s better not to use Codable at all.

First, refer to the previous article on using Associated Value Enum as a container for values.

enum OptionalValue<T: Decodable>: Decodable {
    case null
    case value(T)
    init(from decoder: Decoder) throws {
        let container = try decoder.singleValueContainer()
        if let value = try? container.decode(T.self) {
            self = .value(value)
        } else {
            self = .null
        }
    }
}

Using generics, T represents the actual data field type; .value(T) holds the decoded value, while .null indicates the value is null.

struct Song: Decodable {
    enum CodingKeys: String, CodingKey {
        case id
        case file
    }
    
    var id: Int
    var file: OptionalValue<String>?
    
    init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)
        
        self.id = try container.decode(Int.self, forKey: .id)
        
        if container.contains(.file) {
            self.file = try container.decode(OptionalValue<String>.self, forKey: .file)
        } else {
            self.file = nil
        }
    }
}

var jsonData = """
{
    "id":1
}
""".data(using: .utf8)!
var result = try! JSONDecoder().decode(Song.self, from: jsonData)
print(result)

jsonData = """
{
    "id":1,
    "file":null
}
""".data(using: .utf8)!
result = try! JSONDecoder().decode(Song.self, from: jsonData)
print(result)

jsonData = """
{
    "id":1,
    "file":\"https://test.com/m.mp3\"
}
""".data(using: .utf8)!
result = try! JSONDecoder().decode(Song.self, from: jsonData)
print(result)

The example is simplified to only include two data fields: id and file.

The Song Entity overrides the Decode method by using the contains(.KEY) function to check if the Response includes the field (regardless of its value). If it does, it decodes it into an OptionalValue; within the OptionalValue enum, it further decodes the actual desired value. If decoding succeeds with a value, it is stored in .value(T); if the value is null (or decoding fails), it is stored in .null.

  1. When the Response has a field and a value: OptionalValue.value(VALUE)

  2. When the Response has the field and the value is null: OptionalValue.null

  3. When the Response field is missing: nil

This way, you can distinguish whether a field is provided or not. When writing to Core Data later, you can decide whether to update the field to null or leave it unchanged.

Other Approaches — Double Optional ❌

Optional! Optional! In Swift, it is very suitable for handling this scenario.

struct Song: Decodable {
    var id: Int
    var name: String??
    var file: String??
    var converImage: String??
    var likeCount: Int??
    var like: Bool??
    var length: Int??
}
  1. When the response provides a field & value: Optional(VALUE)

  2. When the response provides the field & the value is null: Optional(nil)

  3. When the Response field is missing: nil

However… Codable JSONDecoder Decode treats both Double Optional and Optional with decodeIfPresent, both considered as Optional, and does not specially handle Double Optional; so the result remains the same as before.

Other Approaches — Property Wrapper ❌

Originally, I expected to use a Property Wrapper for elegant encapsulation, for example:

@OptionalValue var file: String?

But before diving into the details, I found that for Codable properties marked with Property Wrapper, the API response must include that field; otherwise, a keyNotFound error occurs, even if the field is Optional. ?????

The official forum also has a discussion thread about this issue… it is expected to be fixed in the future.

So when choosing packages like BetterCodable or CodableWrappers, you need to consider the current issues with Property Wrappers.

Other Problem Scenarios

Independent writing, free to read — please support these ads

 

Advertise here →

1. How to Decode API Response Using 0/1 to Represent Bool?

import Foundation

struct Song: Decodable {
    enum CodingKeys: String, CodingKey {
        case id
        case name
        case like
    }
    
    var id: Int
    var name: String?
    var like: Bool?
    
    init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)
        self.id = try container.decode(Int.self, forKey: .id)
        self.name = try container.decodeIfPresent(String.self, forKey: .name)
        
        if let intValue = try container.decodeIfPresent(Int.self, forKey: .like) {
            self.like = (intValue == 1) ? true : false
        } else if let boolValue = try container.decodeIfPresent(Bool.self, forKey: .like) {
            self.like = boolValue
        }
    }
}

var jsonData = """
{
    "id": 1,
    "name": "告五人",
    "like": 0
}
""".data(using: .utf8)!
var result = try! JSONDecoder().decode(Song.self, from: jsonData)
print(result)

Extending the previous article, we can decode as int/Bool in the init Decode and assign the value ourselves. This way, the original field can accept 0/1/true/false.

2. Avoid rewriting init decoder every time

Extending the original JSON Decoder to add more features without implementing a custom Decoder yourself.

We can create our own extension for KeyedDecodingContainer to define public methods. Swift will prioritize executing our redefined methods within the module, overriding the original Foundation implementation.

The entire module is affected.

And it’s not a true override—you can’t call super.decode, and be careful not to call yourself (e.g., decode(Bool.Type, for: key) inside decode(Bool.Type, for: key))

There are two methods for decoding:

  • decode(Type, forKey:) Handling Non-Optional Data Fields

  • decodeIfPresent(Type, forKey:) Handling Optional Data Fields

Example 1. The main issue mentioned above can be directly handled with an extension:

extension KeyedDecodingContainer {
    public func decodeIfPresent<T>(_ type: T.Type, forKey key: Self.Key) throws -> T? where T : Decodable {
        // better:
        switch type {
        case is OptionalValue<String>.Type,
             is OptionalValue<Int>.Type:
            return try? decode(type, forKey: key)
        default:
            return nil
        }
        // or just return try? decode(type, forKey: key)
    }
}

struct Song: Decodable {
    var id: Int
    var file: OptionalValue<String>?
}

Since the main issue involves optional data fields and Decodable types, we override the decodeIfPresent<T: Decodable> method.

Here, it is speculated that the original implementation of decodeIfPresent returns nil directly if the data is null or the response does not provide the field, without actually performing decode.

The principle is simple: as long as the Decodable type is OptionValue, it will always attempt to decode, allowing us to get different state results. However, it's also fine not to check the Decodable type, meaning all Optional fields will try to decode.

Example 2. The method can also be extended to Problem Scenario 1:

extension KeyedDecodingContainer {
    public func decodeIfPresent(_ type: Bool.Type, forKey key: KeyedDecodingContainer<K>.Key) throws -> Bool? {
        if let intValue = try? decodeIfPresent(Int.self, forKey: key) {
            return (intValue == 1) ? (true) : (false)
        } else if let boolValue = try? decodeIfPresent(Bool.self, forKey: key) {
            return boolValue
        }
        return nil
    }
}

struct Song: Decodable {
    enum CodingKeys: String, CodingKey {
        case id
        case name
        case like
    }
    
    var id: Int
    var name: String?
    var like: Bool?
}

var jsonData = """
{
    "id": 1,
    "name": "告五人",
    "like": 1
}
""".data(using: .utf8)!
var result = try! JSONDecoder().decode(Song.self, from: jsonData)
print(result)

Conclusion

Most of the clever tricks for using Codable have been tried, some of which are quite convoluted because Codable’s strict constraints sacrifice much of the flexibility needed in real-world development. In the end, I even started questioning why I chose Codable in the first place, as its advantages seem to diminish over time…

References

Review

Independent writing, free to read — please support these ads

 

Advertise here →
Improve this page
Edit on GitHub
Also published on Medium
Read the original
Share this essay
Copy link · share to socials
ZhgChgLi
Author

ZhgChgLi

An iOS, web, and automation developer from Taiwan 🇹🇼 who also loves sharing, traveling, and writing.

Comments