How to add (Swift4) init to Decodable protocol

I am trying to create a Codable extension capable of initializing a Decodable object (Swift 4) with only json string. So what should work:

struct MyObject: Decodable {
   var title: String?
}

let myObject = MyObject(json: "{\"title\":\"The title\"}")

      

I think this means that I have to create an init that calls self.init with the decoder. Here is the code I came up with:

public init?(json: String) throws {
    guard let decoder: Decoder = GetDecoder.decode(json: json)?.decoder else { return }
    try self.init(from: decoder) // Who what should we do to get it so that we can call this?
}

      

This code is able to get a decoder, but I get a compiler error when calling init. The error I am getting:

'self' used before calling self.init

Does this mean that there is no way to add init to the Decodable protocol?

For full source code see the extensible extension for github

update: After debugging the solution below from @appzYourLive I found out that I had a conflict with the initializers init(json:

on Decodable and Array. I just posted a new version of the extension for GitHub. I've also added a solution as a new answer to this question.

+3


source to share


3 answers


Possible workaround

DecodableFromString

Possible solution is defining another protocol

protocol DecodableFromString: Decodable { }

      

with its own initializer

extension DecodableFromString {
    init?(from json: String) throws {
        guard let data = try json.data(using: .utf8) else { return nil }
        guard let value = try? JSONDecoder().decode(Self.self, from: data) else { return nil }
        self = value
    }
}

      

Corresponds to DecodableFromString

Now you need to match the type DecodableFromString



struct Person:Codable, DecodableFromString {
    let firstName: String
    let lastName: String
}

      

Result

And finally got the JSON

let json =  """
            {
            "firstName": "Luke",
            "lastName": "Skywalker"
            }
            """

      

you can create your value

if let luke = try? Person(from: json) {
    print(luke)
}

      

Person (firstName: "Luke", lastName: "Skywalker")

+5


source


This is what can be done:

extension Decodable {

    init?(jsonString: String) throws {
        guard let jsonData = jsonString.data(using: .utf8) else { return nil }
        self = try JSONDecoder().decode(Self.self, from: jsonData)
    }

}

      



See: https://bugs.swift.org/browse/SR-5356

[UPD] Problem fixed in XCode 9 beta 3 (see link above for details).

+2


source


My original problem was caused by two reasons:

  • Conflict between init with identical signature, which I added as an extension to an array, which is also Codable when its internal objects are Encoded.
  • A quick compiler error that causes a problem when using a failover initializer. See https://bugs.swift.org/browse/SR-5356

Since there is a problem using the failable initialiser, I ended up with:

public extension Decodable {
    init(json: String) throws {
        guard let data = json.data(using: .utf8) else { throw CodingError.RuntimeError("cannot create data from string") }
        try self.init(data: data, keyPath: keyPath)
    }

    init(data: Data) throws {
        self = try JSONDecoder().decode(Self.self, from: data)
    }
}

enum CodingError : Error {
    case RuntimeError(String)
}

      

I also made an option where you can use keyPath to navigate to a specific section:

public extension Decodable {
    init(json: String, keyPath: String? = nil) throws {
        guard let data = json.data(using: .utf8) else { throw CodingError.RuntimeError("cannot create data from string") }
        try self.init(data: data, keyPath: keyPath)
    }

    init(data: Data, keyPath: String? = nil) throws {
        if let keyPath = keyPath {
            let topLevel = try JSONSerialization.jsonObject(with: data, options: JSONSerialization.ReadingOptions.mutableContainers)
            guard let nestedJson = (topLevel as AnyObject).value(forKeyPath: keyPath) else { throw CodingError.RuntimeError("Cannot decode data to object")  }
            let nestedData = try JSONSerialization.data(withJSONObject: nestedJson)
            self = try JSONDecoder().decode(Self.self, from: nestedData)
            return
        }
        self = try JSONDecoder().decode(Self.self, from: data)
    }
}

      

Below you can find the complete code for the Decodable and Encodable extensions. This is also on my GitHub subtype. With this extension, you can use code like:

struct YourCodableObject : Codable {
    var naam: String?
    var id: Int?
}

let json = yourEncodableObjectInstance.toJsonString()
let data = yourEncodableObjectInstance.toJsonData()
let newObject = try? YourCodableObject(json: json)
let newObject2 = try? YourCodableObject(data: data)
let objectArray = try? [YourCodableObject](json: json)
let objectArray2 = try? [YourCodableObject](data: data)
let newJson = objectArray.toJsonString()
let innerObject = try? TestCodable(json: "{\"user\":{\"id\":1,\"naam\":\"Edwin\"}}", keyPath: "user")
try initialObject.saveToDocuments("myFile.dat")
let readObject = try? TestCodable(fileNameInDocuments: "myFile.dat")
try objectArray.saveToDocuments("myFile2.dat")
let objectArray3 = try? [TestCodable](fileNameInDocuments: "myFile2.dat")

      

And here are 2 extensions:

//
//  Codable.swift
//  Stuff
//
//  Created by Edwin Vermeer on 28/06/2017.
//  Copyright © 2017 EVICT BV. All rights reserved.
//

enum CodingError : Error {
    case RuntimeError(String)
}

public extension Encodable {
    /**
     Convert this object to json data

     - parameter outputFormatting: The formatting of the output JSON data (compact or pritty printed)
     - parameter dateEncodinStrategy: how do you want to format the date
     - parameter dataEncodingStrategy: what kind of encoding. base64 is the default

     - returns: The json data
     */
    public func toJsonData(outputFormatting: JSONEncoder.OutputFormatting = .compact, dateEncodingStrategy: JSONEncoder.DateEncodingStrategy = .deferredToDate, dataEncodingStrategy: JSONEncoder.DataEncodingStrategy = .base64Encode) -> Data? {
        let encoder = JSONEncoder()
        encoder.outputFormatting = outputFormatting
        encoder.dateEncodingStrategy = dateEncodingStrategy
        encoder.dataEncodingStrategy = dataEncodingStrategy
        return try? encoder.encode(self)
    }

    /**
     Convert this object to a json string

     - parameter outputFormatting: The formatting of the output JSON data (compact or pritty printed)
     - parameter dateEncodinStrategy: how do you want to format the date
     - parameter dataEncodingStrategy: what kind of encoding. base64 is the default

     - returns: The json string
     */
    public func toJsonString(outputFormatting: JSONEncoder.OutputFormatting = .compact, dateEncodingStrategy: JSONEncoder.DateEncodingStrategy = .deferredToDate, dataEncodingStrategy: JSONEncoder.DataEncodingStrategy = .base64Encode) -> String? {
        let data = self.toJsonData(outputFormatting: outputFormatting, dateEncodingStrategy: dateEncodingStrategy, dataEncodingStrategy: dataEncodingStrategy)
        return data == nil ? nil : String(data: data!, encoding: .utf8)
    }


    /**
     Save this object to a file in the temp directory

     - parameter fileName: The filename

     - returns: Nothing
     */
    public func saveTo(_ fileURL: URL) throws {
        guard let data = self.toJsonData() else { throw CodingError.RuntimeError("cannot create data from object")}
        try data.write(to: fileURL, options: .atomic)
    }


    /**
     Save this object to a file in the temp directory

     - parameter fileName: The filename

     - returns: Nothing
     */
    public func saveToTemp(_ fileName: String) throws {
        let fileURL = try FileManager.default.url(for: .cachesDirectory, in: .userDomainMask, appropriateFor: nil, create: false).appendingPathComponent(fileName)
        try self.saveTo(fileURL)
    }



    #if os(tvOS)
    // Save to documents folder is not supported on tvOS
    #else
    /**
     Save this object to a file in the documents directory

     - parameter fileName: The filename

     - returns: true if successfull
     */
    public func saveToDocuments(_ fileName: String) throws {
        let fileURL = try FileManager.default.url(for: .documentDirectory, in: .userDomainMask, appropriateFor: nil, create: false).appendingPathComponent(fileName)
        try self.saveTo(fileURL)
    }
    #endif
}

public extension Decodable {
    /**
     Create an instance of this type from a json string

     - parameter json: The json string
     - parameter keyPath: for if you want something else than the root object
     */
    init(json: String, keyPath: String? = nil) throws {
        guard let data = json.data(using: .utf8) else { throw CodingError.RuntimeError("cannot create data from string") }
        try self.init(data: data, keyPath: keyPath)
    }

    /**
     Create an instance of this type from a json string

     - parameter data: The json data
     - parameter keyPath: for if you want something else than the root object
     */
    init(data: Data, keyPath: String? = nil) throws {
        if let keyPath = keyPath {
            let topLevel = try JSONSerialization.jsonObject(with: data, options: JSONSerialization.ReadingOptions.mutableContainers)
            guard let nestedJson = (topLevel as AnyObject).value(forKeyPath: keyPath) else { throw CodingError.RuntimeError("Cannot decode data to object")  }
            let nestedData = try JSONSerialization.data(withJSONObject: nestedJson)
            let value = try JSONDecoder().decode(Self.self, from: nestedData)
            self = value
            return
        }
        self = try JSONDecoder().decode(Self.self, from: data)
    }

    /**
     Initialize this object from an archived file from an URL

     - parameter fileNameInTemp: The filename
     */
    public init(fileURL: URL) throws {
        let data = try Data(contentsOf: fileURL)
        try self.init(data: data)
    }

    /**
     Initialize this object from an archived file from the temp directory

     - parameter fileNameInTemp: The filename
     */
    public init(fileNameInTemp: String) throws {
        let fileURL = try FileManager.default.url(for: .cachesDirectory, in: .userDomainMask, appropriateFor: nil, create: false).appendingPathComponent(fileNameInTemp)
        try self.init(fileURL: fileURL)
    }

    /**
     Initialize this object from an archived file from the documents directory

     - parameter fileNameInDocuments: The filename
     */
    public init(fileNameInDocuments: String) throws {
        let fileURL = try FileManager.default.url(for: .documentDirectory, in: .userDomainMask, appropriateFor: nil, create: false).appendingPathComponent(fileNameInDocuments)
        try self.init(fileURL: fileURL)
    }
}

      

-1


source







All Articles