1

If an array in JSON is at root level, then code is simple and beautiful:
JSONDecoder().decode([T].self, data)
But how does it work under the hood?
I want to know this in order to implement a custom decoder (with the same calling style) in a case when the array is not at the root level.
For example:

{
    "status": "success",
    "data": {
        "base": {
            "symbol": "USD",
            "sign": "$"
        },
        "coins": [
            {
                "name": "Bitcoin",
                "price": 7783.1949110647,
            },
            {
                "name": "Ethereum",
                "price": 198.4835955777,
            },
            {
                "name": "Tether",
                "price": 1.0026682789,
            },
            {
                "name": "Litecoin",
                "price": 45.9617330332,
            }
        ]
    }
}
struct Coin: Decodable {
    let name: String
    let price: Double

    init(from decoder: Decoder) throws {
        let rootContainer = try decoder.container(keyedBy: CodingKeys.self)
        let nestedContainer = try rootContainer.nestedContainer(keyedBy: CodingKeys.self, forKey: .data)
        var unkeyedContainer = try nestedContainer.nestedUnkeyedContainer(forKey: .coins)
        let coinContainer = try unkeyedContainer.nestedContainer(keyedBy: CodingKeys.self)
        name = try coinContainer.decode(String.self, forKey: .name)
        price = try coinContainer.decode(Double.self, forKey: .price)
    }

    enum CodingKeys: String, CodingKey {
        case data
        case coins
        case name
        case price
    }
}

It almost works!
When .decode(Coin.self, data) it just returns single, the very first element in the array.
When .decode([Coin].self, data) sadly, but it throws the error:

Expected to decode Array, but found a dictionary instead.

Looks like I've missed some last step to make it works in a way I want.

Roman
  • 1,309
  • 14
  • 23

4 Answers4

0

You could define the structures to mirror your JSON:

struct ResponseObject: Codable {
    let status: String
    let data: Currency
}

struct CurrencyBase: Codable {
    let symbol: String
    let sign: String
}

struct Currency: Codable {
    let base: CurrencyBase
    let coins: [Coin]
}

struct Coin: Codable {
    let name: String
    let price: Double
}

Then you can .decode(ResponseObject.self, from: data).

Rob
  • 415,655
  • 72
  • 787
  • 1,044
0

try this

import UIKit

class ViewController: UIViewController {

    override func viewDidLoad() {
        super.viewDidLoad()
        // Do any additional setup after loading the view.



    let input = """
     {
        "status": "success",
        "data": {
            "base": {
                "symbol": "USD",
                "sign": "$"
            },
            "coins": [
                {
                    "name": "Bitcoin",
                    "price": 7783.1949110647,
                },
                {
                    "name": "Ethereum",
                    "price": 198.4835955777,
                },
                {
                    "name": "Tether",
                    "price": 1.0026682789,
                },
                {
                    "name": "Litecoin",
                    "price": 45.9617330332,
                }
            ]
        }
    }
    """

    let decodedData = try? JSONDecoder().decode(TotalData.self, from: input.data(using: .utf8)!)

    print ("\(String(describing: decodedData))")
}


    struct Coin: Decodable {
        let name: String
        let price: Double
    }

    struct Base: Decodable {
        let symbol: String
        let sign: String
    }

    struct CoinsData: Decodable {
        let base: Base
        let coins: [Coin]
    }

    struct TotalData: Decodable {
        let status: String
        let data: CoinsData
    }

}
0

Make a type to get rid of the containing nonsense, and then you can keep Coin clean!

struct Coin: Decodable {
  let name: String
  let price: Double
}

extension Coin {
  struct : Decodable {
    enum CodingKey: Swift.CodingKey { case data, coins }

    init(from decoder: Decoder) throws {
      coins = try .init(container:
        decoder.container(keyedBy: CodingKey.self)
        .nestedContainer(keyedBy: CodingKey.self, forKey: .data)
        .nestedUnkeyedContainer(forKey: .coins)
      ) { try $0.decode(Coin.self) }
    }

    let coins: [Coin]
  }
}

try JSONDecoder().decode(Coin..self, from: data).coins

( looks like Kirby but is a purse.)

public extension Array {
  /// Iterate through an `UnkeyedDecodingContainer` and create an `Array`.
  /// - Parameters:
  ///   - iterate: Mutates `container` and returns an `Element`, or `throw`s.
  init(
    container: UnkeyedDecodingContainer,
    iterate: (inout UnkeyedDecodingContainer) throws -> Element
  ) throws {
    try self.init(
      initialState: container,
      while: { !$0.isAtEnd },
      iterate: iterate
    )
  }
}
public extension Array {
  /// A hack to deal with `Sequence.next` not being allowed to `throw`.
  /// - Parameters:
  ///   - initialState: Mutable state.
  ///   - continuing: Check the state to see if iteration is complete.
  ///   - iterate: Mutates the state and returns an `Element`, or `throw`s.
  init<State>(
    initialState: State,
    while continuing: @escaping (State) -> Bool,
    iterate: (inout State) throws -> Element
  ) throws {
    var state = initialState
    self = try
      Never.ending.lazy
      .prefix { continuing(state) }
      .map { try iterate(&state) }
  }
}
public extension Never {
  /// An infinite sequence whose elements don't matter.
  static var ending: AnySequence<Void> { .init { } }
}
public extension AnySequence {
  /// Use when `AnySequence` is required / `AnyIterator` can't be used.
  /// - Parameter getNext: Executed as the `next` method of this sequence's iterator.
  init(_ getNext: @escaping () -> Element?) {
    self.init( Iterator(getNext) )
  }
}
0

Not exactly what I was trying to achieve in my initial question of this post, but is also a very satisfying code:

struct Container: Decodable, IteratorProtocol, Sequence {
    private var unkeyedContainer: UnkeyedDecodingContainer
    var coin: Coin?

    init(from decoder: Decoder) throws {
        let rootContainer = try decoder.container(keyedBy: CodingKeys.self)
        let nestedContainer = try rootContainer.nestedContainer(keyedBy: CodingKeys.self, forKey: .data)
        unkeyedContainer = try nestedContainer.nestedUnkeyedContainer(forKey: .coins)
    }

    mutating func next() -> Coin? {
        guard !unkeyedContainer.isAtEnd else { return nil }
        if let coin = try? unkeyedContainer.decode(Coin.self) { return coin } else { return nil }
    }

    enum CodingKeys: String, CodingKey {
        case data
        case coins
    }
}

struct Coin: Decodable {
    let name: String
    let price: Double
}

Usage:

let coins: [Coins]
let decoder = JSONDecoder()
coins = Array(decoder.decode(Container.self, data))

That's it. It works! Thank you everyone for hints.

Roman
  • 1,309
  • 14
  • 23
  • I wouldn't advise holding onto an `unkeyedContainer` like that. The container probably holds onto the parent decoder, which probably holds onto the full decoded data (JSON, YAML or whatever). If the `Container` lives for long, then that's effectively a memory leak (holding onto a large chunk of memory, only to ever use a small aspect of it). Instead, it would be better to just loop inside `init(from decoder: Decoder)`, extract the elements of `unkeyedContainer`, and store them in a `var coins: [Coin]` property. – Alexander May 02 '20 at 03:20
  • Here's a simplified example of that: https://gist.github.com/amomchilov/053e120553c8af42f2687288f2a47eb3 – Alexander May 02 '20 at 03:43