$30 off During Our Annual Pro Sale. View Details »

Otemachi.swift#04 今から始めるCodable移行

Otemachi.swift#04 今から始めるCodable移行

Go Takagi

July 30, 2019
Tweet

More Decks by Go Takagi

Other Decks in Programming

Transcript

  1. 今から始めるCodable移⾏
    ⾼⽊ 豪 ⽇本経済新聞社
    Otemachi.swift #

    View Slide

  2. ‣ Go Takagi
    • 新卒1年⽬、新⽶
    • 好きな⾔語: Go
    • @shimastriper
    ‣ 最近の悩み
    • iMacPro貸与されてから快適で帰る気が起きない
    • 新卒がiMacPro使えるイイカイシャデスヨ!!(≧∇≦)b
    I am...
    2

    View Slide

  3. 今⽇は少し泥臭い話をします
    3

    View Slide

  4. 4
    皆さんCodable使ってますか?

    View Slide

  5. ‣ EntityとWebAPI(JSON)を簡潔にマッピング
    Codable
    5 https://developer.apple.com/documentation/foundation/jsondecoder
    struct GroceryProduct: Codable {
    var name: String
    var points: Int
    var description: String?
    }
    let json = """
    {
    "name": "Durian",
    "points": ,
    "description": "A fruit with a distinctive scent."
    }
    """.data(using: .utf )!
    let decoder = JSONDecoder()
    let product = try decoder.decode(GroceryProduct.self, from: json)
    print(product.name) // Prints "Durian"

    View Slide

  6. ‣ EntityとWebAPI(JSON)を簡潔にマッピング
    Codable
    6 https://developer.apple.com/documentation/foundation/jsondecoder
    相互にデータを変換!!
    Entity
    JSON
    struct GroceryProduct: Codable {
    var name: String
    var points: Int
    var description: String?
    }
    let json = """
    {
    "name": "Durian",
    "points": ,
    "description": "A fruit with a distinctive scent."
    }
    """.data(using: .utf )!
    let decoder = JSONDecoder()
    let product = try decoder.decode(GroceryProduct.self, from: json)
    print(product.name) // Prints "Durian"

    View Slide

  7. 7
    ガンガン使っていきましょう!!

    View Slide

  8. が、、、
    8
    既存からの移⾏は結構⼤変...

    View Slide

  9. ‣ APIの構造がEntityと1:1対応してないとCodableは⾟い
    • 前処理: 値を変換して⼊れるとか
    • 値を演算してから⼊れる
    • 複数のキーを利⽤ / 失敗を握りつぶす
    • どちらかのキーに⼊っているものだけ利⽤
    • 変換失敗する要素は握りつぶす配列
    • キーが不定: このキーないとき無視してnil代⼊
    • ⼤きいJSONの⼀部だけEntityにする
    • 1つのリクエストから複数のEntityに分割

    どういうところが⼤変か
    9

    View Slide

  10. Decode/Encodeを⾃前で書くと...
    10
    struct GroceryProduct: Codable {
    let name: String
    let points: Int
    enum CodingKeys: String, CodingKey {
    case name
    case points
    }
    }
    extension GroceryProduct: Decodable {
    init(from: Decoder) throws {
    let values = try decoder.container(keyedBy: CodingKeys.self)
    self.name = try values.decode(String.self, forKey: .name)
    self.points = try values.decode(Int.self, forKey: .points)
    }
    }
    extension GroceryProduct: Encodable {
    func encode(to encoder: Encoder) throws {
    var container = encoder.container(keyedBy: CodingKeys.self)
    try container.encode(self.name, forKey: .name)
    try container.encode(self.points, forKey: .points)
    }
    }

    View Slide

  11. Decode/Encodeを⾃前で書くと...
    11
    struct GroceryProduct: Codable {
    let name: String
    let points: Int
    enum CodingKeys: String, CodingKey {
    case name
    case points
    }
    }
    extension GroceryProduct: Decodable {
    init(from: Decoder) throws {
    let values = try decoder.container(keyedBy: CodingKeys.self)
    self.name = try values.decode(String.self, forKey: .name)
    self.points = try values.decode(Int.self, forKey: .points)
    }
    }
    extension GroceryProduct: Encodable {
    func encode(to encoder: Encoder) throws {
    var container = encoder.container(keyedBy: CodingKeys.self)
    try container.encode(self.name, forKey: .name)
    try container.encode(self.points, forKey: .points)
    }
    }
    冗⻑な記述を
    書くのを避けたい
    処理する段階に応じて
    できるだけ冗⻑さを避ける

    View Slide

  12. ‣ API側で担保すべきことも往々にしてあるが...
    • Androidと共通利⽤してるとAPI側を直すのも⼀苦労
    • init(from Decoder)/ func encode(to: Encoder)の⽅法を

    毎回書くと冗⻑......
    • 簡潔にできることはないか?
    ‣ ケースに基づく対応⽅法を紹介
    • ⽇経の90EntitiesをCodable対応して知⾒をまとめました
    • もともと⽇経は github.com/JohnSundell/Unbox でJSONを扱ってました
    • クライアント側単体でなるべく冗⻑さを回避して旨味を享受する
    歴史ある仕様に柔軟に対応しないといけないとき
    12

    View Slide

  13. ‣ Codableとは
    ‣ JSONDecoder / JSONEncoder で全体の⼀貫した設定
    • snake_case、Date型、pretty format...
    ‣ キーのマッピングはCodingKeysで指定
    • 丁寧に指定しておくと簡潔になることも
    ‣ それでもダメならdecode / encode⽅法を⾃前で書く
    ‣ ハマりどころ‧懸念
    • 配列の要素を操作する場合は気をつけないと無限ループする
    • Performance Issueとか
    検討⼿順 (⾚字に⾄る前に解決したいのが肝です)
    13

    View Slide

  14. ‣ データ表現を相互に変換するためのProtocol
    • A type that can convert itself into and out of an external
    representation.
    • 今回はJSON
    • 標準の型はもちろんCodableに準拠するデータ構造で

    フィールドを構成していればCodableにできる
    • OptionalにすればJSONのnullもマッピング可能
    Codableとは
    14
    struct GroceryProduct: Codable {
    let name: String
    let points: Int
    let description: String?
    let myStruct: MyStruct
    }
    struct MyStruct: Codable {
    var name: String
    }
    let json = """
    {
    "name": "Durian",
    "points": ,
    "description": "A fruit with a distinctive scent."
    "myStruct": {
    "name": "Durian"
    }
    }
    """.data(using: .utf )!

    View Slide

  15. Decode/Encodeを⾃前で書くと...
    15
    struct GroceryProduct: Codable {
    let name: String
    let points: Int
    enum CodingKeys: String, CodingKey {
    case name
    case points
    }
    }
    extension GroceryProduct: Decodable {
    init(from: Decoder) throws {
    let values = try decoder.container(keyedBy: CodingKeys.self)
    self.name = try values.decode(String.self, forKey: .name)
    self.points = try values.decode(Int.self, forKey: .points)
    }
    }
    extension GroceryProduct: Encodable {
    func encode(to encoder: Encoder) throws {
    var container = encoder.container(keyedBy: CodingKeys.self)
    try container.encode(self.name, forKey: .name)
    try container.encode(self.points, forKey: .points)
    }
    }
    冗⻑な記述を
    書くのを避けたい
    処理する段階に応じて
    できるだけ冗⻑さを避ける

    View Slide

  16. struct GroceryProduct: Codable {
    let name: String
    let points: Int
    let description: String?
    let myStruct: MyStruct
    }
    struct MyStruct: Codable {
    var name: String
    }
    let json = """
    {
    "name": "Durian",
    "points": ,
    "description": "A fruit with a distinctive scent."
    "myStruct": {
    "name": "Durian"
    }
    }
    """.data(using: .utf )!
    ‣ keyDecodingStrategy / keyEncodingStrategy
    • JSONのKeyの記法を変換してからマップできる
    • camelCase, snake_caseに限らず独⾃定義の変換も可能
    • Swift . (Xcode . )から利⽤可能
    DecodeやEncodeのデータ全体に対する設定①
    16
    Ex. JSONDecoder().keyDecodingStrategy = .snakeCaseToCamelCase

    View Slide

  17. struct GroceryProduct: Codable {
    let name: String
    let points: Int
    let description: String?
    let myStruct: MyStruct
    }
    struct MyStruct: Codable {
    var name: String
    }
    let json = """
    {
    "name": "Durian",
    "points": ,
    "description": "A fruit with a distinctive scent."
    "my_struct": {
    "name": "Durian"
    }
    }
    """.data(using: .utf )!
    ‣ keyDecodingStrategy / keyEncodingStrategy
    • JSONのKeyの記法を変換してからマップできる
    • camelCase, snake_caseに限らず独⾃定義の変換も可能
    • Swift . (Xcode . )から利⽤可能
    DecoderやEncodeでデータ全体に対する設定①
    17
    Ex. JSONDecoder().keyDecodingStrategy = .snakeCaseToCamelCase
    ࣗಈͰϚοϐϯά

    View Slide

  18. ‣ 他にも
    • DateDecodingStrategy / DateEncodingStrategy
    • Date型のフォーマットを指定できる
    • DataDecodingStrategy / DataEncodingStrategy
    • バイナリ型のフォーマットを指定
    • OutputFormatting
    • JSONEncoderからpretty formatで出⼒
    • NonConformingFloatEncodingStrategy
    • 浮動⼩数点数を扱う
    DecodeやEncodeでデータ全体に対する設定②
    18

    View Slide

  19. ‣ Decode/Encodeのキーとフィールドが⼀致しない際利⽤
    • `enum CodingKeys`を定義するとそれに従う
    • `case field = "jsonKey"`でマップ
    • 基本的にcaseはフィールド名と1:1で対応させる
    • それ以上でも以下でもdecode⽅法とencode⽅法の

    実装が求められるようになる
    キー名の違いはCodingKeysで解決
    19
    struct GroceryProduct: Codable {
    let name: String
    let points: Int
    enum CodingKeys: String, CodingKey {
    case name = "otherName"
    case points = "otherPoints"
    }
    }
    let json = """
    {
    "other_name": "Durian",
    "other_points":
    }
    """.data(using: .utf )!

    View Slide

  20. ‣ 追加で使うキーは別のCodingKeysにする
    • ⾃前で書く必要がない処理を省略できる
    フィールドと対応させることの効能
    20
    struct GroceryProduct: Codable {
    let name: String
    let points: Int
    enum CodingKeys: String, CodingKey {
    case name
    case points
    }
    enum AdditionalCodingKeys: String, CodingKey {
    case usingOnlyDecode
    }
    }
    init(from decoder: Decoder)
    のみで利⽤
    encodeは
    ⾃動推論してくれる

    View Slide

  21. ‣ decodeやencode⽅法を⾃分で書く
    それでも前処理したかったら
    21
    extension GroceryProduct: Decodable {
    init(from: Decoder) throws {
    let values = try decoder.container(keyedBy: CodingKeys.self)
    self.name = try values.decode(String.self, forKey: .name)
    self.points = try values.decode(Int.self, forKey: .points)
    }
    }
    extension GroceryProduct: Encodable {
    func encode(to encoder: Encoder) throws {
    var container = encoder.container(keyedBy: CodingKeys.self)
    try container.encode(self.name, forKey: .name)
    try container.encode(self.points, forKey: .points)
    }
    }

    View Slide

  22. tips: 配列のDecodeできない要素を握りつぶす
    22
    struct GroceryProductList: Decodable {
    let products: [GroceryProduct]
    init(from decoder: Decoder) throws {
    var products: [GroceryProduct] = []
    var unkeyedContainer = try decoder.unkeyedContainer()
    while !unkeyedContainer.isAtEnd {
    let product = try unkeyedContainer.decode(GroceryProduct.self)
    products.append(product)
    }
    self.products = products
    }
    }
    ‣ 配列要素のDecode失敗を握りつぶしたい
    配列の要素をDecode
    ここでtry catchすればいいのでは?

    View Slide

  23. 実は無限ループが起きる
    23
    struct GroceryProductList: Decodable {
    let products: [GroceryProduct]
    init(from decoder: Decoder) throws {
    var products: [GroceryProduct] = []
    var unkeyedContainer = try decoder.unkeyedContainer()
    while !unkeyedContainer.isAtEnd {
    let product = try unkeyedContainer.decode(GroceryProduct.self)
    products.append(product)
    }
    self.products = products
    }
    }
    ‣ Containerのループのインクリメントがされない
    • 同じパースをし続けて⼀⽣失敗
    decode成功しないと

    ループが進まない!
    ref: https://github.com/apple/swift/blob/master/stdlib/public/Darwin/Foundation/JSONEncoder.swift#L
    guard let decoded = try self.decoder.unbox...
    throw DecodingError.valueNotFound...
    }
    self.currentIndex +=
    swift側の実装

    View Slide

  24. nilを成功扱いするstructでwrapすると対処可能
    24 ref: https://github.com/JohnSundell/Codextended/pull/
    private struct IgnoreFailureDecodable: Decodable {
    let element: T?
    init(from decoder: Decoder) throws {
    let container = try decoder.singleValueContainer()
    self.element = try? container.decode(T.self)
    }
    }
    let container = try self.container(keyedBy: CodingKeys.self)
    let decodedArray =
    try container.decode([IgnoreFailureDecodable].self, forKey: .array)
    return decodedArray.compactMap({ $0.element })
    失敗してもnil扱いで
    Parse成功するstruct
    nilを消去

    View Slide

  25. Performance Issue
    25
    ‣ CodableはPerformanceに直結して響く
    • decode / encodeを⾃前で書くか推論させるか
    • データ構造の複雑さによって

    どっちのほうが早いか安定してない記事がちらほら...
    • ⽇経のEntityは後者のほうが10倍早かった (実測値)
    • JSONにEntityと関係ないデータがたくさんあるか
    • JSON→Dictionary→Entityと変換して必要箇所を参照するほうが早い
    • 移⾏したら忘れずにInstrumentsで測定 !!
    decodeを定義 Codableが推論

    View Slide

  26. ‣ Codableは便利だから使ってほしい
    • しかし、移⾏する場合WebAPI次第で結構苦労する
    ‣ 冗⻑な記述を避ける⼿順
    • 全体のParseするルールの⼀貫性を設定
    • snake_caseをここで解決できることは嬉しい
    • CodingKeysをFieldと1:1対応させる
    • ⾃前で書く必要があるdecode/encodeを記述
    • Parseできない配列要素を握りつぶす場合テクニックが要る
    • Performanceにsensitiveに影響、Instrumentsは忘れずに
    まとめ
    26

    View Slide