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.
点击这里查看本文章简体中文版本。
點擊這裡查看本文章正體中文版本。
This post was translated with AI assistance — let me know if anything sounds off!
Real-World Codable Decoding Issues Summary (Part 2)
Properly handling Response Null fields does not always require rewriting the init decoder.
Photo by Zan
Introduction
Following the previous article “Practical Issues Encountered with Codable Decoding,” development has progressed further, bringing new scenarios and challenges. This follow-up continues to document the situations encountered and the research process for future reference.
The previous article mainly addressed JSON String -> Entity Object Decodable Mapping. With 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 Issues
Assuming our song Entity structure is as follows:
1
2
3
4
5
6
7
8
9
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 may not always return complete data fields (only the id is guaranteed), all fields except for id are optional. For example, when fetching song information, the full structure is returned, but when liking or favoriting a song, only the three related fields id
, likeCount
, and like
are returned.
We want all fields from the API response to be saved into Core Data. If the data already exists, update only the changed fields (incremental update).
But here comes the problem: after converting Codable Decode to Entity Object, we cannot distinguish between “the data field is intended to be set to nil” and “the response did not provide it”
1
2
3
4
5
A Response:
{
"id": 1,
"file": null
}
For both A Response and B Response, the file is null, but their meanings are different; A intends to set the file field to null (clearing the original data), while B intends to update other data and simply does not provide the file field.
The Swift community has developers proposing adding a null Strategy similar to date Strategy in JSONDecoder, allowing us to distinguish the above situations, but there are currently no plans to add it.
Solution
As mentioned earlier, our architecture is JSON String -> Entity Object -> NSManagedObject, so when we get the Entity Object, it is already the decoded result, and there is no raw data to manipulate; of course, we can use the original JSON String for comparison here, but rather than doing that, it’s better not to use Codable.
First, refer to the previous article for using Associated Value Enum as a container to hold values.
1
2
3
4
5
6
7
8
9
10
11
12
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.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
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
andfile
.
The Song Entity overrides the Decode method by itself, using the contains(.KEY)
method to check if the Response includes the field (regardless of its value). If it exists, it decodes it into an OptionalValue; the OptionalValue Enum then attempts to decode the actual value we want. If decoding succeeds, it stores it as .value(T)
; if the value is null (or decoding fails), it stores it as .null
.
When Response has a field & value: OptionalValue.value(VALUE)
When the Response has a field & value as null: OptionalValue.null
Response when no field is given: nil
This way, you can tell if a field is provided or not. When writing to Core Data later, you can decide whether to update the field to null or skip updating it.
Other Studies — Double Optional ❌
Optional! Optional! Swift is well suited to handle this scenario.
1
2
3
4
5
6
7
8
9
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 includes a field & value: Optional(VALUE)
When the Response has a field & value as null: Optional(nil)
When Response does not provide a field: nil
However… Codable JSONDecoder Decode handles both Double Optional and Optional with decodeIfPresent, treating them as Optional, so the result remains the same.
Other Research — Property Wrapper ❌
Originally planned to use Property Wrapper for elegant encapsulation, for example:
1
@OptionalValue var file: String?
But before diving into the details, I found that if a Codable property is marked with a Property Wrapper, the API response must include that field; otherwise, a keyNotFound error will occur, even if the field is Optional. ?????
The official forum also has a discussion thread about this issue… it is expected to be fixed later.
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?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
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 into int/Bool ourselves in init Decode and assign the values manually. 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 building a Decoder from scratch.
We can extend KeyedDecodingContainer to define our own public methods. Swift will prioritize executing the methods we redefine in our module, overriding the original Foundation implementation.
The entire module is affected.
And it’s not a true override, so you can’t call super.decode. Be careful not to call yourself (e.g., decode(Bool.Type, for: key) inside decode(Bool.Type, for: key))
There are two methods for decode:
decode(Type, forKey:) Handling Non-Optional Data Fields
decodeIfPresent(Type, forKey:) handles optional data fields
Example 1. The main issue mentioned above can be directly extended:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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>?
}
The main issue involves Optional data fields and Decodable types, so we override the decodeIfPresent<T: Decodable> method.
Here it is translated according to your instructions:
It is speculated that the original implementation of decodeIfPresent returns nil directly if the data is null or the Response does not provide it, without actually running decode.
So the principle is simple: as long as the Decodable Type is OptionValue
Example 2. Problem Scenario 1 can also be expanded using this method:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
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 are quite convoluted because Codable’s constraints are very strict, sacrificing 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
If you have any questions or feedback, feel free to contact me.
This post was originally published on Medium (View original post), and automatically converted and synced by ZMediumToMarkdown.