Using JSONEncoder to encode a variable with Codable as type

13,096

Solution 1

Use a generic type constrained to Encodable

func saveObject<T : Encodable>(_ object: T, at location: String) {
    //Some code

    let data = try JSONEncoder().encode(object)

    //Some more code
}

Solution 2

I would use a different approach to that extending the Encodable protocol with all the instance methods you might need. Expanding on that you can add a parameter to your methods to pass a custom encoder and provide a default encoder as well to all of them:

extension DataProtocol {
    var string: String? { String(bytes: self, encoding: .utf8) }
}

extension Encodable {
    func data(using encoder: JSONEncoder = JSONEncoder()) throws -> Data { try encoder.encode(self) }
    func string(using encoder: JSONEncoder = JSONEncoder()) throws -> String { try data(using: encoder).string ?? "" }
}

Usage

let message = ["key":["a","b","c"]]

let jsonData = try! message.data() // 21 bytes [123, 34, 107, 101, 121, 34, 58, 91, 34, 97, 34, 44, 34, 98, 34, 44, 34, 99, 34, 93, 125]
let jsonString = try! message.string()  // "{"key":["a","b","c"]}"

Example when passing a date with a default encoder. Note that the dateEncodingStrategy used is the default (a Double representing the timeIntervalSinceReferenceDate):

let message = ["createdAt": Date()]

let jsonData = try! message.data() // 33 bytes -> [123, 34, 99, 114, 101, 97, 116, 101, 97, 100, 65, 116, 34, 58, 53, 55, 49, 54, 49, 55, 56, 52, 49, 46, 52, 53, 48, 55, 52, 52, 48, 51, 125]
let jsonString = try! message.string()  // {"createdAt":571617841.45074403}"

Now you can pass a custom encoder to your method to format your Date in a human readable format:

let message = ["createdAt": Date()]
let encoder = JSONEncoder()
encoder.dateEncodingStrategy = .iso8601
let jsonString = try! message.string(using: encoder)  // "{"createdAt":"2019-02-11T22:48:19Z"}"

Now using a custom Message structure

struct Message: Codable {
    let id: Int
    let createdAt: Date
    let sender, title, body: String
}

extension Encodable {
    func sendDataToServer(using encoder: JSONEncoder = JSONEncoder()) throws {
        print(self, terminator: "\n\n")
        // Don't handle the error here. Propagate the error.
        let data = try self.data(using: encoder)
        print(data.string!)
        // following the code to upload the data to the server
        print("Message was successfully sent")
    }
}

let message = Message(id: 1, createdAt: Date(), sender: "[email protected]", title: "Lorem Ipsum", body: """
Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries, but also the leap into electronic typesetting, remaining essentially unchanged. It was popularised in the 1960s with the release of Letraset sheets containing Lorem Ipsum passages, and more recently with desktop publishing software like Aldus PageMaker including versions of Lorem Ipsum.
""")

let iso8601 = JSONEncoder()
iso8601.dateEncodingStrategy = .iso8601
iso8601.outputFormatting = .prettyPrinted
do {
    try message.sendDataToServer(using: iso8601)
} catch {
    // handle all errors
    print(error)
}

This will print

Message(id: 1, createdAt: 2019-02-11 23:57:31 +0000, sender: "[email protected]", title: "Lorem Ipsum", body: "Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry\'s standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries, but also the leap into electronic typesetting, remaining essentially unchanged. It was popularised in the 1960s with the release of Letraset sheets containing Lorem Ipsum passages, and more recently with desktop publishing software like Aldus PageMaker including versions of Lorem Ipsum.")

{
  "body" : "Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries, but also the leap into electronic typesetting, remaining essentially unchanged. It was popularised in the 1960s with the release of Letraset sheets containing Lorem Ipsum passages, and more recently with desktop publishing software like Aldus PageMaker including versions of Lorem Ipsum.",
  "id" : 1,
  "sender" : "[email protected]",
  "title" : "Lorem Ipsum",
  "createdAt" : "2019-02-11T23:57:31Z"
}
now just add the code to send the json data to the server
Share:
13,096
Denis Balko
Author by

Denis Balko

Updated on August 04, 2022

Comments

  • Denis Balko
    Denis Balko almost 2 years

    I managed to get both JSON and plist encoding and decoding working, but only by directly calling the encode/decode function on a specific object.

    For example:

    struct Test: Codable {
        var someString: String?
    }
    
    let testItem = Test()
    testItem.someString = "abc"
    
    let result = try JSONEncoder().encode(testItem)
    

    This works well and without issues.

    However, I am trying to get a function that takes in only the Codable protocol conformance as type and saves that object.

    func saveObject(_ object: Encodable, at location: String) {
        // Some code
    
        let data = try JSONEncoder().encode(object)
    
        // Some more code
    }
    

    This results in the following error:

    Cannot invoke 'encode' with an argument list of type '(Encodable)'

    Looking at the definition of the encode function, it seems as if it should be able to accept Encodable, unless Value is some strange type I don't know of.

    open func encode<Value>(_ value: Value) throws -> Data where Value : Encodable
    
  • Denis Balko
    Denis Balko almost 7 years
    Of course, thank you, I am still new to Swift and I completely forgot this is how it's done.
  • Erik Mueller
    Erik Mueller over 6 years
    what the? could someone explain me this behaviour? it doesn't make any sense to me
  • Ash
    Ash about 6 years
    Codable needs to be able to determine its object type. Using Any as the type confuses it because it doesn't know which type's init(from decoder:) initialiser to call. This function essentially provides the missing information in the form of a generic. The code can calculate which type to use through type inference.
  • David H
    David H almost 6 years
    I had big hopes for this but same error: ` let bufferReq: Encodable func toData<T: Encodable>(object: T) throws -> Data { let encoder = JSONEncoder() return try encoder.encode(object) } do { let foo = toData(object: bufferReq) // still has same error `
  • Siempay
    Siempay almost 6 years
    it need to know (or to infer) the type you wanna encode, because if you have var x: Encodable = myXStruct(); and var y: Encodable = myYStruct(); . the encode function wont know what is that. is it x or y. so you should look up generic functions and Protocols with associated types (PAT)
  • pkamb
    pkamb over 5 years
    What is the syntax for doing this in a property getter rather than a function?
  • vadian
    vadian over 5 years
    @pkamb Sorry I don't understand what you mean.
  • pkamb
    pkamb over 5 years
    Something like var object<T : Encodable>: T {, which doesn't compile.
  • Leo Dabus
    Leo Dabus over 5 years
    @pkamb you can not declare a computed property as generic and/or make it throw an error but you can declare it as an instance property extending the Encodable protocol (you would need to handle or ignore your error there).