170

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?

pkamb
  • 33,281
  • 23
  • 160
  • 191
RamwiseMatt
  • 2,717
  • 3
  • 16
  • 22
  • Are you saying you have a case where you have some properties which you want to encode, but different properties which you want to decode? (i.e. You want your type to not be round-trippable?) Because if you just care about excluding the property, giving it a default value and leaving it out of the `CodingKeys` enum is sufficient. – Itai Ferber Jun 20 '17 at 14:48
  • 1
    Regardless, you can always implement the requirements of the `Codable` protocol (`init(from:)` and `encode(to:)`) manually for full control over the process. – Itai Ferber Jun 20 '17 at 14:49
  • My specific use-case is to avoid giving a decoder too much control, which could lead to remotely obtained JSON from overwriting internal property values. The solutions below are adequate! – RamwiseMatt Jun 21 '17 at 15:35
  • 3
    I'd like to see an answer / new Swift feature that only requires handling the special cases and excluded keys, rather than re-implementing all of the properties that you should normally get for free. – pkamb Apr 23 '18 at 17:02

7 Answers7

294

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)
    }
}
Code Different
  • 90,614
  • 16
  • 144
  • 163
  • 26
    You need to give `nickname` a default value for this to work. Otherwise, there's no value that can be assigned to the property on `init(from:)`. – Itai Ferber Jun 20 '17 at 22:11
  • 3
    @ItaiFerber I switched it to an optional, which was in my Xcode originally – Code Different Jun 20 '17 at 22:27
  • Are you sure you have to provide the `encode` in the asymmetric example? Since that is still the standard behavior, I didn't think it was needed. Just the `decode` since that's where the asymmetry is coming from. – Mark A. Donohoe Jan 22 '18 at 16:07
  • 1
    @MarqueIV Yes, you have to. Since `fullName` cannot be mapped to a stored property, you must provide a custom encoder and decoder. – Code Different Mar 01 '18 at 03:01
  • 2
    Just tested this in Swift 5. You should only need to define a constant for the property you which not to decode. You do not need to explicitly add the keys to `CodingKeys`. So, `var nickname: String { get { "name" } }` should suffice. – Leo Dec 30 '20 at 21:06
  • I works fine but I got into a trouble for the private access modifier, so I just removed it. Just in case anybody runs into this problem too. – jguillen Dec 29 '21 at 21:44
  • Xcode can now (at least as of Xcode 14, maybe earlier) generate this implementation for you. Right-click on the class definition → Refactor → "Add Explicit Codable Implementation" – FeifanZ Mar 28 '23 at 01:18
  • @Leo sorry what? Is there a way to do the top example in the answer without defining CodingKeys to exclude nickname, assuming it's a normal readwrite prop? – Dan Rosenstark Jul 26 '23 at 22:41
43

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
    }
}
iuriimoz
  • 951
  • 10
  • 11
  • 1
    This seems to perfectly fit the situation where you want almost everything to use the default implementation of Codability, except for a member or two. It would be a shame to incur the complexity of an entire custom Codable conformance, and this prevents that by encapsulating the complexity elsewhere and making the usage site appropriately minimal. Property wrappers FTW – Porter Child Jul 05 '22 at 04:09
  • 1
    Are the two extensions for `KeyedDecodingContainer` and `KeyedEncodingContainer` really needed? Without them, encoding and decoding works fine in my case. – Nickkk Sep 02 '22 at 14:13
  • Yes they are needed, without this code a field that is marked with @CodableIgnored can not be parsed by decoder. – Ivan Jul 17 '23 at 07:27
7

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

  • Does the decoder automatically choose the right container when performing decoding or should i define a condition logic for that? – Coder Feb 19 '21 at 09:18
  • @Citizen_5 decoder has logic to use both codingkey enums (regular CodingKeys and custom one), but encoder only knows about CodingKeys, that's why it works this way. – Rukshan Jul 18 '21 at 15:54
5

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.

4

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
  }
}
Beta-Logics
  • 324
  • 1
  • 3
  • 11
  • 4
    This was the clue for me - using a `lazy var` effectively making it a runtime property excluded it from Codable. – ChrisH Sep 16 '20 at 01:24
  • Computed properties can't be lazy, can you expand your line of thought? @ChrisH – Bruno Muniz Nov 30 '21 at 23:44
  • @BrunoMuniz honestly cannot remember why I would have made this comment, but I would most likely have been referring to something like `lazy var x:String = { //Computed string }()` which I used instead of a computed property although I can't really imagine why. – ChrisH Dec 01 '21 at 05:43
0

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 #ids 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 JSONand 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.

Patru
  • 4,481
  • 2
  • 32
  • 42
0

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)

infiniteLoop
  • 2,135
  • 1
  • 25
  • 29