Real-world Codable Decode Issues Summary (Part 2)
Handling Response Null Fields Properly Doesn’t Always Require Overriding init Decoder

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:
idandfile.
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.
-
When the Response has a field and a value: OptionalValue.value(VALUE)
-
When the Response has the field and the value is null: OptionalValue.null
-
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??
}
-
When the response provides a field & value: Optional(VALUE)
-
When the response provides the field & the value is null: Optional(nil)
-
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
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
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…



Comments