Home > database >  Multiple Encodable implementations
Multiple Encodable implementations

Time:11-05

Say I have an object, Game, which contains three properties, score, firstName, lastName.

 class Game: Encodable {

    var firstName: String
    var lastName: String
    var score: Int

    enum CodingKeys: String, CodingKey {
        case firstName
        case lastName
        case score
    }

    func encode(to encoder: Encoder) throws {
        var encoder = encoder.container(keyedBy: CodingKeys.self)
        try? encoder.encode(self.firstName, forKey: .firstName)
        try? encoder.encode(self.lastName, forKey: .lastName)
        try? encoder.encode(self.score, forKey: .score)
    }
 }

Say I want to persist this to disk, I can use JSONEncoder

let game = Game(firstName: "John", lastName: "Smith", score: 42)
let data = try JSONEncoder().encode(game)
{
    firstName: "John",
    lastName: "Smith",
    score: 42
}

Now I want users to be able to generate the JSON on their own and share it to any app, but for privacy reasons I want to omit the last name, how could I create two implementations of encode(to encoder: Encoder) for the one type?

Example Output:

{
    firstName: "John",
    score: 42
}

CodePudding user response:

You can add an additional variable to your class (make sure not to add this additional variable to CodingKeys enum) and use it in your encode(to encoder: function as shown

class Game: Encodable { // assuming you have assigned initial values to all your properties or you have a init which initializes all these properties, I would have used struct but am not sure of your usecase so leaving it as is

   var firstName: String
   var lastName: String
   var score: Int
   var isLastNameEnabled: Bool

   enum CodingKeys: String, CodingKey {
       case firstName
       case lastName
       case score
   }

   func encode(to encoder: Encoder) throws {
       var encoder = encoder.container(keyedBy: CodingKeys.self)
       try? encoder.encode(self.firstName, forKey: .firstName)
       if isLastNameEnabled {
           try? encoder.encode(self.lastName, forKey: .lastName)
       }
       try? encoder.encode(self.score, forKey: .score)
   }
}

Finally, if you want to add last name as part of your JSON use

        let game = Game(firstName: "John", lastName: "Smith", score: 42, isLastNameEnabled: true)
        do {
            let data = try JSONEncoder().encode(game)
            let json = String(bytes: data, encoding: .utf8)
            print(json)
        }
        catch {
            print(error) //handle error here
        }

O/P:

{ "firstName": "John", "score": 42, "lastName": "Smith" }

if you want to avoid last name use

        let game = Game(firstName: "John", lastName: "Smith", score: 42, isLastNameEnabled: false)
        do {
            let data = try JSONEncoder().encode(game)
            let json = String(bytes: data, encoding: .utf8)
            print(json)
        }
        catch {
            print(error) // handle error here
        }

O/P:

{ "firstName":"John", "score":42 }

CodePudding user response:

There're some options. Besides Sandeep's solution with a Bool flag, polymorphism might be another one:

final class ShareableGame: Game {

  init(_ game: Game) {
    super.init(firstName: game.firstName,
               lastName: game.lastName,
               score: game.score)
  }

  override func encode(to encoder: Encoder) throws {
    var encoder = encoder.container(keyedBy: CodingKeys.self)
    try? encoder.encode(self.firstName, forKey: .firstName)
    try? encoder.encode(self.score, forKey: .score)
  }

}

let game = Game(firstName: "John", lastName: "Smith", score: 42)
let shareableGame = ShareableGame(game)
let shareableGameData = try JSONEncoder().encode(shareableGame)
print(String(bytes: shareableGameData, encoding: .utf8)!)

// {"firstName":"John","score":42}

(Assuming the Game class is defined similarly to this:)

class Game: Encodable {

  let firstName: String
  let lastName: String
  let score: Int

  init(firstName: String, lastName: String, score: Int) {
    self.firstName = firstName
    self.lastName = lastName
    self.score = score
  }

  /*
   encode(to:) throws can be omitted, since the default     
   implementation is exactly as expected.
  */

  enum CodingKeys: String, CodingKey {
    // Cannot be omitted, since it's used in subclasses.

    case firstName
    case lastName
    case score

  }

}

CodePudding user response:

You could make use of the userInfo dictionary in the JSONEncoder to set a flag whether this is a private encoding or not. This way you don't need to add properties to your type or create another one.

First we need a key for the dictionary, I have made a more general one here

let isPrivateInfoKey = CodingUserInfoKey(rawValue: "isPrivate")!

and then we need to check it in the encode function

func encode(to encoder: Encoder) throws {
    var container = encoder.container(keyedBy: CodingKeys.self)
    let isPrivate = encoder.userInfo[isPrivateInfoKey] as? Bool ?? true

    try container.encode(self.firstName, forKey: .firstName)
    if isPrivate {
        try container.encode(self.lastName, forKey: .lastName)
    }
    try container.encode(self.score, forKey: .score)
}

Default is true if the key/value hasn't been added to the dictionary so it only needs to be set when the last name shouldn't be included

let encoder = JSONEncoder()
encoder.userInfo[isPrivateInfoKey] = false
do {
    let game = Game(firstName: "John", lastName: "Smith", score: 42)
    let data = try encoder.encode(game)
    if let s = String(data: data, encoding: .utf8) { print(s) }
} catch {
    print(error)
}
  • Related