Как реализовать пользовательский декодер, в случае структуры массива JSON, с быстрым Decodable? - PullRequest
1 голос
/ 30 апреля 2020

Если массив в JSON находится на уровне root, то код прост и красив:
JSONDecoder().decode([T].self, data)
Но как это работает под капотом?
Я хочу знать это для реализации пользовательского декодера ( с тем же стилем вызова ) в случае, когда массив не находится на уровне root.
Например:

{
    "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
    }
}

Это почти работает!
Когда .decode(Coin.self, data) возвращает только один, самый первый элемент в массиве.
Когда .decode([Coin].self, data), к сожалению, но выдает ошибку:

Ожидается декодировать массив, но вместо этого нашел словарь.

Похоже, я пропустил какой-то последний шаг, чтобы заставить его работать так, как я хочу.

Ответы [ 4 ]

0 голосов
/ 02 мая 2020

Не совсем то, чего я пытался достичь в своем первоначальном вопросе к этому посту, но это также очень приятный код:

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
}

Использование:

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

Вот и все. Оно работает! Спасибо всем за подсказки.

0 голосов
/ 30 апреля 2020

попробуйте

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 голосов
/ 30 апреля 2020

Сделайте тип, чтобы избавиться от содержащейся ерунды, и тогда вы сможете сохранить Coin в чистоте! 100

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

( ? выглядит как Кирби, но это кошелек.)

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 голосов
/ 30 апреля 2020

Вы можете определить структуры для отражения вашего 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
}

Тогда вы можете .decode(ResponseObject.self, from: data).

...