Reputation: 2957
Swift's Encodable
/Decodable
protocols, released with Swift 4, make JSON (de)serialization quite pleasant. However, I have not yet found a way to have fine-grained control over which properties should be encoded and which should get decoded.
I have noticed that excluding the property from the accompanying CodingKeys
enum excludes the property from the process altogether, but is there a way to have more fine-grained control?
Upvotes: 187
Views: 73151
Reputation: 1081
Solution with custom property wrapper
struct Person: Codable {
var firstName: String
var lastName: String
@CodableIgnored
var nickname: String?
}
Where CodableIgnored
is
@propertyWrapper
public struct CodableIgnored<T>: Codable {
public var wrappedValue: T?
public init(wrappedValue: T?) {
self.wrappedValue = wrappedValue
}
public init(from decoder: Decoder) throws {
self.wrappedValue = nil
}
public func encode(to encoder: Encoder) throws {
// Do nothing
}
}
extension KeyedDecodingContainer {
public func decode<T>(
_ type: CodableIgnored<T>.Type,
forKey key: Self.Key) throws -> CodableIgnored<T>
{
return CodableIgnored(wrappedValue: nil)
}
}
extension KeyedEncodingContainer {
public mutating func encode<T>(
_ value: CodableIgnored<T>,
forKey key: KeyedEncodingContainer<K>.Key) throws
{
// Do nothing
}
}
Upvotes: 55
Reputation: 93161
The list of keys to encode/decode is controlled by a type called CodingKeys
(note the s
at the end). The compiler can synthesize this for you but can always override that.
Let's say you want to exclude the property nickname
from both encoding and decoding:
struct Person: Codable {
var firstName: String
var lastName: String
var nickname: String?
private enum CodingKeys: String, CodingKey {
case firstName, lastName
}
}
If you want it to be asymmetric (i.e. encode but not decode or vice versa), you have to provide your own implementations of encode(with encoder: )
and init(from decoder: )
:
struct Person: Codable {
var firstName: String
var lastName: String
// Since fullName is a computed property, it's excluded by default
var fullName: String {
return firstName + " " + lastName
}
private enum CodingKeys: String, CodingKey {
case firstName, lastName, fullName
}
// We don't want to decode `fullName` from the JSON
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
firstName = try container.decode(String.self, forKey: .firstName)
lastName = try container.decode(String.self, forKey: .lastName)
}
// But we want to store `fullName` in the JSON anyhow
func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(firstName, forKey: .firstName)
try container.encode(lastName, forKey: .lastName)
try container.encode(fullName, forKey: .fullName)
}
}
Upvotes: 308
Reputation: 81
Another way to exclude some properties from encoder, separate coding container can be used
struct Person: Codable {
let firstName: String
let lastName: String
let excludedFromEncoder: String
private enum CodingKeys: String, CodingKey {
case firstName
case lastName
}
private enum AdditionalCodingKeys: String, CodingKey {
case excludedFromEncoder
}
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
let anotherContainer = try decoder.container(keyedBy: AdditionalCodingKeys.self)
firstName = try container.decode(String.self, forKey: .firstName)
lastName = try container.decode(String.self, forKey: .lastName)
excludedFromEncoder = try anotherContainer(String.self, forKey: . excludedFromEncoder)
}
// it is not necessary to implement custom encoding
// func encode(to encoder: Encoder) throws
// let person = Person(firstName: "fname", lastName: "lname", excludedFromEncoder: "only for decoding")
// let jsonData = try JSONEncoder().encode(person)
// let jsonString = String(data: jsonData, encoding: .utf8)
// jsonString --> {"firstName": "fname", "lastName": "lname"}
}
same approach can be used for decoder
Upvotes: 8
Reputation: 374
You can use computed properties:
struct Person: Codable {
var firstName: String
var lastName: String
var nickname: String?
var nick: String {
get {
nickname ?? ""
}
}
private enum CodingKeys: String, CodingKey {
case firstName, lastName
}
}
Upvotes: 6
Reputation: 934
If we need to exclude decoding of a couple of properties from a large set of properties in the structure, declare them as optional properties. Code to unwrapping optionals is less than writing a lot of keys under CodingKey enum.
I would recommend using extensions to add computed instance properties and computed type properties. It separates codable comforming properties from other logic hence provides better readability.
Upvotes: 5
Reputation: 2183
I have used protocol and its extension along with AssociatedObject to set and get image (or any property which needs to be excluded from Codable) property.
With this we dont have to implement our own Encoder and Decoder
Here is the code, keeping relevant code for simplicity:
protocol SCAttachmentModelProtocol{
var image:UIImage? {get set}
var anotherProperty:Int {get set}
}
extension SCAttachmentModelProtocol where Self: SCAttachmentUploadRequestModel{
var image:UIImage? {
set{
//Use associated object property to set it
}
get{
//Use associated object property to get it
}
}
}
class SCAttachmentUploadRequestModel : SCAttachmentModelProtocol, Codable{
var anotherProperty:Int
}
Now, whenever we want to access the Image property we can use on the object confirming to protocol (SCAttachmentModelProtocol)
Upvotes: 0
Reputation: 4551
While this can be done it ultimately ends up being very unSwifty and even unJSONy. I think I see where you are coming from, the concept of #id
s is prevalent in HTML, but it is rarely transported over to the world of JSON
which I consider a good thing (TM).
Some Codable
structs will be able to parse your JSON
file just fine if you restructure it using recursive hashes, i.e. if your recipe
just contains an array of ingredients
which in turn contains (one or several) ingredient_info
. That way the parser will help you to stitch your network together in the first place and you only have to provide some backlinks through a simple traversal the structure if you really need them. Since this requires a thorough rework of your JSON
and your data structure I only sketch out the idea for you to think about it. If you deem it acceptable please tell me in the comments then I could elaborate it further, but depending on the circumstances you may not be at the liberty to change either one of them.
Upvotes: 0