How to decode a "Dictionary" to "[Model]"?
CoffeeWu opened this issue · 3 comments
CoffeeWu commented
// 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!
gfontenot commented
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.
- In
Other
, decodeinfos
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 theinfos
key from the JSON (which gives usDecoded<JSON>
and thenflatMap
that intodecodeObject
which will give usDecoded<[String: JSON]>
. This is a good place for error handling if you want (you could return a.failure
here ifinfos
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
}
}
- 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
}
}
- In the new
User.decode
function, pass the key as the id byproduct wrapping it inpure
(because we know it exists), and then decode the rest of the JSON normally. For this example, I'll just try to decode theJSON
directly to aString
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)
}
}
- Back up in the
Other.decode
function, usemap
to iterate over the keys/values in theinfos
dict into the newUser.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
}
}
- Finally, we can use
sequence
to transform[Decoded<User>]
intoDecoded<[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)
}
}
CoffeeWu commented
Wow, functional programming is so cool~ Thank you very much for the detailed and understandable answer!!!
gfontenot commented
Glad that helped! I'm going to close this issue, but feel free to continue asking questions here or by opening a new issue.