swiftencodable

How to use different keys in input and output when using Encodable protocol?


Expected this: name -> fullname

struct Person: Codable {
    let name: String

    enum CodingKeys: String, CodingKey {
        case name = "fullname"
    }

    init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)
        self.name = try container.decode(String.self, forKey: .name)
    }

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

let jsonString = """
{
    "name": "John"
}
"""

let jsonDecoder = JSONDecoder()
if let jsonData = jsonString.data(using: .utf8),
   let decodedPerson = try? jsonDecoder.decode(Person.self, from: jsonData) {
    print(decodedPerson.name) // Output: "John"
}

let jsonEncoder = JSONEncoder()
jsonEncoder.outputFormatting = .prettyPrinted
if let encodedData = try? jsonEncoder.encode(decodedPerson),
   let encodedString = String(data: encodedData, encoding: .utf8) {
    print(encodedString)
}

But the reality that second output produce this:

{
  "name" : "John"
}

Solution

  • If your goal is to decode the name key and output the fullname key (which is really confusing and means your code can't decode the JSON that it generates) then you need to update the CodingKeys to:

    enum CodingKeys: String, CodingKey {
        case fullname = "fullname"
        case name = "name"
    }
    

    and you need to update the encode method to use .fullname instead of .name.

    Then you can parse the original JSON with the name key and then generate new JSON with a fullname key.

    Here's your code with some cleanup and the changes I suggest:

    struct Person: Codable {
        let name: String
    
        enum CodingKeys: String, CodingKey {
            case fullname = "fullname"
            case name = "name"
        }
    
        init(from decoder: Decoder) throws {
            let container = try decoder.container(keyedBy: CodingKeys.self)
            self.name = try container.decode(String.self, forKey: .name)
        }
    
        func encode(to encoder: Encoder) throws {
            var container = encoder.container(keyedBy: CodingKeys.self)
            try container.encode(name, forKey: .fullname) // Use fullname
        }
    }
    
    let jsonString = """
    {
        "name": "John"
    }
    """
    
    do {
        let jsonData = jsonString.data(using: .utf8)! // this can't fail
        let jsonDecoder = JSONDecoder()
        let decodedPerson = try jsonDecoder.decode(Person.self, from: jsonData)
        print(decodedPerson.name) // Output: "John"
    
        let jsonEncoder = JSONEncoder()
        jsonEncoder.outputFormatting = .prettyPrinted
        let encodedData = try jsonEncoder.encode(decodedPerson)
        let encodedString = String(data: encodedData, encoding: .utf8)! // This won't fail either
        print(encodedString)
    } catch {
        print(error)
    }
    

    The output is:

    John
    {
    "fullname" : "John"
    }


    Just to reiterate. This is a bad idea. Your Person class, with these changes, can't decode the JSON that it encodes. Are you really sure that is what you want?