thoughtbot/Argo

How to decode a "Dictionary" to "[Model]"?

CoffeeWu opened this issue · 3 comments

// Sample JSON
{
    "ids": ["id1", "id2"],
    "infos": {
        "id1": "name1",
        "id2": "name2"
    }
}
// id1/id2   in “ids” is alway equals to id1/id2   in “infos”
// id1, id2 will change, not the exact key

Question: I want to transfer every key-value pair of "infos" to a User model, 
how to implement  the "decode" method?

// Models
struct Other {
    let ids: [String]?
    let users:  [User]?
}

extension Other: Argo.Decodable {
  static func decode(_ json: JSON) -> Decoded< Other > {
        return curry(Other.init)
        <^> json <||?  “ids”
        <*> json <|?   “infos”                   // how?
}

struct User {
    let id: String
    let name: String
}

extension User: Argo.Decodable {
  static func decode(_ json: JSON) -> Decoded<User> {
        return curry(User.init)
        <^> json <| ""            // id1 ?   how to apply all the key?
        <*> json <| ""            // name1 ? 
}

First time to use Argo, thanks to solve my problem!

Unfortunately, this isn't super easy. It's going to take a few steps. I'm going to go ahead and lay this out under the assumption that your User object isn't represented as a simple [String: String] dictionary.

  1. In Other, decode infos to [String: JSON]. We're going to need to do this because we don't know what the values actually are. This will also let Argo take care of more complicated decoding if it can down the line. To make this work, we'll need to pull out the infos key from the JSON (which gives us Decoded<JSON> and then flatMap that into decodeObject which will give us Decoded<[String: JSON]>. This is a good place for error handling if you want (you could return a .failure here if infos doesn't contain an object), but for the sake of this example we're going to pull out the value and supply a default value of an empty dictionary:
extension Other: Argo.Decodable {
  static func decode(_ json: JSON) -> Decoded<Other> {
    let infos: [String: JSON] = ((json <| "infos") >>- decodeObject).value ?? [:]
    // snip
  }
}
  1. Create a new initializer for User that takes the id as a separate parameter. Since we are storing the key outside of the JSON, we'll need to pass it in separately as well:
extension User { // No need to make this Decodable now
  static func decode(_ json: JSON, key: String) -> Decoded<User> {
    // snip
  }
}
  1. In the new User.decode function, pass the key as the id byproduct wrapping it in pure (because we know it exists), and then decode the rest of the JSON normally. For this example, I'll just try to decode the JSON directly to a String value:
extension User { // No need to make this Decodable now
  static func decode(_ json: JSON, key: String) -> Decoded<User> {
    return curry(User.init)
      <^> pure(key)
      <*> String.decode(json)
  }
}
  1. Back up in the Other.decode function, use map to iterate over the keys/values in the infos dict into the new User.decode method, which will result in a value of the type [Decoded<User>]:
extension Other: Argo.Decodable {
  static func decode(_ json: JSON) -> Decoded<Other> {
    let infos: [String: JSON] = ((json <| "infos") >>- decodeObject).value ?? [:]
    let decodedInfos: [Decoded<User>] = infos.map { key, json in
      User.decode(json, key: key)
    }

    // snip
  }
}
  1. Finally, we can use sequence to transform [Decoded<User>] into Decoded<[User]> which we can then pass to the initializer (wrapping in .optional in order to make it failable):
extension Other: Argo.Decodable {
  static func decode(_ json: JSON) -> Decoded<Other> {
    let infos: [String: JSON] = ((json <| "infos") >>- decodeObject).value ?? [:]
    let decodedInfos: [Decoded<User>] = infos.map { key, json in
      User.decode(json, key: key)
    }

    return curry(Other.init)
      <^> json <||? "ids"
      <*> .optional(sequence(decodedInfos))
  }
}

Full solution:

struct Other {
    let ids: [String]?
    let users:  [User]?
}

extension Other: Argo.Decodable {
  static func decode(_ json: JSON) -> Decoded<Other> {
    let infos: [String: JSON] = ((json <| "infos") >>- decodeObject).value ?? [:]
    let decodedInfos: [Decoded<User>] = infos.map { key, json in
      User.decode(json, key: key)
    }

    return curry(Other.init)
      <^> json <||? "ids"
      <*> .optional(sequence(decodedInfos))
  }
}

struct User {
  let id: String
  let name: String
}

extension User {
  static func decode(_ json: JSON, key: String) -> Decoded<User> {
    return curry(User.init)
      <^> pure(key)
      <*> String.decode(json)
  }
}

Wow, functional programming is so cool~ Thank you very much for the detailed and understandable answer!!!

Glad that helped! I'm going to close this issue, but feel free to continue asking questions here or by opening a new issue.