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

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

9bf153d8c0ce36ba6d9d20c2914b70f4?s=128

Go Takagi

July 30, 2019
Tweet

Transcript

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

  2. ‣ Go Takagi • 新卒1年⽬、新⽶ • 好きな⾔語: Go • @shimastriper

    ‣ 最近の悩み • iMacPro貸与されてから快適で帰る気が起きない • 新卒がiMacPro使えるイイカイシャデスヨ!!(≧∇≦)b I am... 2
  3. 今⽇は少し泥臭い話をします 3

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

  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"
  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"
  7. 7 ガンガン使っていきましょう!!

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

  9. ‣ APIの構造がEntityと1:1対応してないとCodableは⾟い • 前処理: 値を変換して⼊れるとか • 値を演算してから⼊れる • 複数のキーを利⽤ /

    失敗を握りつぶす • どちらかのキーに⼊っているものだけ利⽤ • 変換失敗する要素は握りつぶす配列 • キーが不定: このキーないとき無視してnil代⼊ • ⼤きいJSONの⼀部だけEntityにする • 1つのリクエストから複数のEntityに分割 • どういうところが⼤変か 9
  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) } }
  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) } } 冗⻑な記述を 書くのを避けたい 処理する段階に応じて できるだけ冗⻑さを避ける
  12. ‣ API側で担保すべきことも往々にしてあるが... • Androidと共通利⽤してるとAPI側を直すのも⼀苦労 • init(from Decoder)/ func encode(to: Encoder)の⽅法を


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

    ‣ キーのマッピングはCodingKeysで指定 • 丁寧に指定しておくと簡潔になることも ‣ それでもダメならdecode / encode⽅法を⾃前で書く ‣ ハマりどころ‧懸念 • 配列の要素を操作する場合は気をつけないと無限ループする • Performance Issueとか 検討⼿順 (⾚字に⾄る前に解決したいのが肝です) 13
  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 )!
  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) } } 冗⻑な記述を 書くのを避けたい 処理する段階に応じて できるだけ冗⻑さを避ける
  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
  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 ࣗಈͰϚοϐϯά
  18. ‣ 他にも • DateDecodingStrategy / DateEncodingStrategy • Date型のフォーマットを指定できる • DataDecodingStrategy

    / DataEncodingStrategy • バイナリ型のフォーマットを指定 • OutputFormatting • JSONEncoderからpretty formatで出⼒ • NonConformingFloatEncodingStrategy • 浮動⼩数点数を扱う DecodeやEncodeでデータ全体に対する設定② 18
  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 )!
  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は ⾃動推論してくれる
  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) } }
  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すればいいのでは?
  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側の実装
  24. nilを成功扱いするstructでwrapすると対処可能 24 ref: https://github.com/JohnSundell/Codextended/pull/ private struct IgnoreFailureDecodable<T: Decodable>: 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<T>].self, forKey: .array) return decodedArray.compactMap({ $0.element }) 失敗してもnil扱いで Parse成功するstruct nilを消去
  25. Performance Issue 25 ‣ CodableはPerformanceに直結して響く • decode / encodeを⾃前で書くか推論させるか •

    データ構造の複雑さによって
 どっちのほうが早いか安定してない記事がちらほら... • ⽇経のEntityは後者のほうが10倍早かった (実測値) • JSONにEntityと関係ないデータがたくさんあるか • JSON→Dictionary→Entityと変換して必要箇所を参照するほうが早い • 移⾏したら忘れずにInstrumentsで測定 !! decodeを定義 Codableが推論
  26. ‣ Codableは便利だから使ってほしい • しかし、移⾏する場合WebAPI次第で結構苦労する ‣ 冗⻑な記述を避ける⼿順 • 全体のParseするルールの⼀貫性を設定 • snake_caseをここで解決できることは嬉しい

    • CodingKeysをFieldと1:1対応させる • ⾃前で書く必要があるdecode/encodeを記述 • Parseできない配列要素を握りつぶす場合テクニックが要る • Performanceにsensitiveに影響、Instrumentsは忘れずに まとめ 26