Anviking / Decodable

[Probably deprecated] Swift 2/3 JSON unmarshalling done (more) right
MIT License
1.04k stars 73 forks source link

Initialise one object from different JSON sources. #29

Open lutzmi opened 8 years ago

lutzmi commented 8 years ago

Say we have an object, which should be initialised from a JSON coming over the network and from a different JSON e.g. coming from the disk. This is currently not possible since, the decode function has no way of knowing the "type" of the JSON it is getting.

Adding type information to the Decodable protocol would allow for having different decode functions to work with the same object:

public protocol Decodable {
    typealias DataSourceType
    static func decode(json: AnyObject, ofType type:DataSourceType.Type) throws -> Self
}

// It could look something like this...
struct TheObject {
    let info:String
}

struct ServerJSON {}
extension TheObject : Decodable {
    typealias DataSourceType = ServerJSON
    static func decode(json: AnyObject, ofType type:DataSourceType.Type) throws -> TheObject
    {
        return TheObject(info: "fromServer")
    }
}

struct DiskJSON {}
extension TheObject {
    static func decode(json: AnyObject, ofType type:DiskJSON.Type) throws -> TheObject
    {
        return TheObject(info: "fromDisk")
    }
}

let o = try! TheObject.decode(["Some":"Thing"], ofType: ServerJSON.self)
o.info // -> "fromServer"

let p = try! TheObject.decode(["Some":"Thing"], ofType: DiskJSON.self)
p.info // -> "fromDisk"
lutzmi commented 8 years ago

... but I guess unless we have really generic protocols and not just the typealias workaround, that's going to be tough to implement in a good way :-(

JaviSoto commented 8 years ago

This is a legitimate concert however. I recently encountered a similar need, when a backend service exposes the same entity in 2 different formats (sucks, but it happens)

Anviking commented 8 years ago

I haven't had to deal with this myself yet. But doesn't skipping adopting the Decodable-protocol work fine? It may not be super beautiful but still:

extension TheObject { // Not conforming to Decodable
    func decode1(json: AnyObject) throws -> TheObject { ... }
    func decode2 ... // But better function names

   // Or only one function but with extra parameters as you did
   // This could also be a curried function to get a closure to pass to the 
   // decode parameter in -parse<T>
   func decode(json: AnyObject, type: AnEnumOrSomething)
}

// Either
let json = ["a" : ["b": ["Some": "Thing"]]]
let a = TheObject.decode1(json => "a" => "b")
// or 
let a = parse(json, ["a", "b"], decode: TheObject.decode2)

Or one could potentially start overloading the parse<T>-function, which I have thought about for #26. This would make it similar to TheObject.decode(json, type: something).

func parse(json: AnyObject, path: [String], type: something) throws -> TheObject

edit forgot path argument in last example

lutzmi commented 8 years ago

I think the problem with the different decode functions, that you want the type information to "trickle down" from parent to child objects (so that they know as well with what kind/type of json they are dealing with). Not sure if it's anywhere clear what I mean by that ;-), but I guess in essence it would mean, that using your json => "a" => "b" example from above the => operator would need to pass on the type of the json parameter (if it had a type), so that the second => knows the type as well.

On the other hand I think you are right that one could work around the issue in a less "beautiful" way. Currently I am achieving a similar things by having different overloaded initialisers like init?(json:,ofType:) for the objects using SwiftyJSON for the parsing. I was/am specifically looking for a more elegant or "beautiful' solution ;-).

Anviking commented 8 years ago

Oh, now I think I see what you did in your example. You leveraged the type-system to determine the decode-function at compile-time, and thats why you use struct-types instead of enums? But doesn't that work excellent? The first => operator shouldn't need to pass any information to the next one, since all of the decoding is done in the first one where as the following ones just append keys to the keypath.

Anviking commented 8 years ago

Understood the toughness you spoke of when trying to replicate this in a playground. It's a really cool idea/problem though. And perhaps it could be solved in another way. Keeping this open :)

Anviking commented 8 years ago

I think this would be a nice approach of handling it, but doesn't work now

import UIKit
import Decodable

// We can't include operator overloads in protocol-extensions but we can do this, just that it doesn't work either.
// Imagine every operator overload was using DynamicDecodable instead, and the Decodable-protocol extends that protocol
protocol DynamicDecodable {
    static var decodeClosure: (json: AnyObject) throws -> Self {get}

}

// Temporary hack to avoid ambiguity
infix operator =>> { associativity right precedence 150 }
func =>> <T: DynamicDecodable>(lhs: AnyObject, rhs: String) throws -> T {
    // toJSONPathArray is private, only one key
    return try parse(lhs, path: [rhs], decode: T.decodeClosure)
}

// One of our new custom protocols
protocol ServerDecodable {
    static func fromServerJSON(json: AnyObject) throws -> Self
}

extension ServerDecodable {
    static var decodeClosure: (json: AnyObject) throws -> Self {
        return fromServerJSON
    }
}

// Models
struct User: ServerDecodable {
    let login: String

    static func fromServerJSON(json: AnyObject) throws -> User {
        return try User(login: json => "login")
    }
}

struct Repository: ServerDecodable {
    let name: String
    let user: User

    static func fromServerJSON(json: AnyObject) throws -> Repository {
        // Error: Argument for generic parameter 'T' could not be inferred
        return try Repository(name: "test", user: json =>> "user")
    }
}

Edit: I'm stupid

voidref commented 8 years ago

Edit: I have no idea what I was talking about here, obviously the decode function I gave would need to take an additional parameter in my example.

DELETED

voidref commented 8 years ago

If Decodable wants to solve this problem, it may be an idea to explore adding an 'or' operator. I'll do some exploration, but it would look like this at the call site:

TheObject {
    let name:String
    class func decode(json:AnyObject) -> throws {
        return try TheObject(name: json => "name || json => "netName")
    }
}
voidref commented 8 years ago

Thinking about the problem further, I don't think it's going to be possible for my above syntax to work, as the => operator will either throw, or it won't, and I don't think we can have an operator that intercepts this throw and tries again.

Another syntax, although not as nice, and definitely more 'magic' feeling would be to have a nested array for alternate key names, thusly:

TheObject {
    let name:String
    class func decode(json:AnyObject) -> throws {
        return try TheObject(name: json => [["name,"netName"]])
    }
}

The nested array is because Decodable supports an array for a path, so a top level array of alternates isn't going to work, specifying a path and alternate keys might look like [["name", "netName"], "first"]

voidref commented 8 years ago

From an implementation standpoint, my above suggestion seems ... rather complex, bordering on unmaintainable/untenable/impractical. I started an exploration into what would be required, and it involves changing the path string type to be an AnyObject type and include type-checking with an asymmetric recursive traversal to cover possible use cases.

I've actually done something like this for some natural language processing as well as chord shape generation, and I really wouldn't wish the maintenance of this sort of thing on anyone.

So, uh ... never mind.

I think the real solution to this lies in #95 / 1, which is seems our dear Viking friend is in the midst of!

valeriomazzeo commented 6 years ago

What about having a context parameter in the decode function?