-1

I just learned Argo basics and was able to decode 99% of my JSONs in production. Now I am facing the following structure (the keys like "5447" and "5954" are dynamic) and need help:

{
  "5447": {
    "business_id": 5447,
    "rating": 5,
    "comment": "abcd",
    "replies_count": 0,
    "message_id": 2517
  },
  "5954": {
    "business_id": 5954,
    "rating": 3,
    "comment": "efgh",
    "replies_count": 0,
    "message_id": 633
  }
}

The typical sample of Argo decoding is like:

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

for JSON structure (keys are fixed "id" and "name"):

{
  "id": 124,
  "name": "someone"
}

using something like this:

extension User: Decodable {
  static func decode(j: JSON) -> Decoded<User> {
    return curry(User.init)
      <^> j <| "id"
      <*> j <| "name"
  }
}

However the data structure I need to parse doesn't fit the example.

UPDATE: using Tony's first implementation with a small modification in the last line, I got my job done. Here is the complete working code:

Business.swift:

import Argo
import Curry
import Runes

struct Business {
    let businessID: Int
    let rating: Double?
    let comment: String?
    let repliesCount: Int?
    let messageID: Int?
}

extension Business: Decodable {
    static func decode(_ json: JSON) -> Decoded<Business> {
        let c0 = curry(Business.init)
            <^> json <| "business_id"
            <*> json <|? "rating"
        return c0
            <*> json <|? "comment"
            <*> json <|? "replies_count"
            <*> json <|? "message_id"
    }
}

Businesses.swift

import Argo
import Runes

struct Businesses {
    let businesses: [Business]
}

extension Businesses: Decodable {
    static func decode(_ json: JSON) -> Decoded<Businesses> {
        let dict = [String: JSON].decode(json)
        let arr = dict.map { Array($0.map { $1 }) }
        let jsonArr = arr.map { JSON.array($0) }
        return Businesses.init <^> jsonArr.map([Business].decode).value ?? .success([])
    }
}
Golden Thumb
  • 2,531
  • 21
  • 20

1 Answers1

4

When you have keys in a dictionary that are dynamic, you'll have to step out of the convenient operators that Argo provides. You'll first need to get the JSON object element then map over it yourself. Since the keys are irrelevant here (because the ids are also in the embedded dictionaries) it actually won't be too bad. The easiest way is to probably make a new struct to wrap this:

struct Businesses: Decodable {
  let businesses: [Business]

  static func decode(_ json: JSON) -> Decoded<Businesses> {
    // Get dictionary
    let dict: Decoded<[String: JSON]> = [String: JSON].decode(json)
    // Transform dictionary to array
    let array: Decoded<[JSON]> = dict.map { Array($0.map { $1 }) }
    // Wrap array back into a JSON type
    let jsonArray: Decoded<JSON> = array.map { JSON.array($0) }
    // Decode the JSON type like we would with no key
    return Businesses.init <^> array.map([Business].decode)
  }
}

What we're doing here is getting the dictionary and transforming it to an array so we can decode it like any other array.

You could also skip the transformation to an array part and decode it from the dictionary like so:

static func decode(_ json: JSON) -> Decoded<Businesses> {
  // Get dictionary
  let dict: Decoded<[String: JSON]> = [String: JSON].decode(json)
  // Map over the dictionary and decode the values
  let result: Decoded<[Businesses]> = dict.flatMap { object in
    let decoded: [Decoded<Business>] = Array(object.map { decode($1) })
    return sequence(decoded)
  }
  return Businesses.init <^> result
}

I didn't try any of this code so there might be some tweaks. Also, you probably don't need all the type annotations but I added them to help explain the code. You might also be able to use this code outside of a new struct model depending on your application.

  • Thanks for the great work on Argo and the detailed sample code for my question! I have difficulty to understand the last line in the first block of code. Could you shed more light? BTW jsonArray in the previous line isn't used. – Golden Thumb May 06 '17 at 03:26
  • Just tried the 1st way Tony suggested. With a small tweak, it works like a charm. I only need to replace the last line with: return Businesses.init <^> jsonArray.map([Business].decode).value ?? .success([]) – Golden Thumb May 06 '17 at 08:07