30
22

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

JSONDecoder,Decodableを使ってみよう

Last updated at Posted at 2018-11-13

JSONDecoder,Decodableを使ってみよう

使っちゃたんだけど引き継ぎ時には忘れてるハズなのでメモです。
JSONDecoder、というよりDecodableをより都合よく使おうというお話です。

コード見ればという人はこちら

概要

swiftでJSONから構造体へ落とせるJSONDecoder/Codable便利ですね。

今までJSONからの構造体への変換を手書きしたり、定義ファイルからコード生成していたりしたのですが、これで楽に、のはずがちょっと問題点もあったりします。

jsonのdataからしか変換できない

JSONDecoderのドキュメントによると

func decode<T>(T.Type, from: Data) -> T

とJSON文字列のData化したものからしか受付できません。
JSONSerializationで変換したAnyなオブジェクトから受付してくれないのはなにかと不便です。(内部で真っ先にJSONSerializationしてるんですけどね)

該当データがないと例外

{"a1": 1}

struct AA: Codable {
	var a1: Int
	var a2: Int
}

とあった場合(上部が入力サンプルjsonデータ、下部が構造体定義としJSONDecoderした場合)、キー"a2"がjsonに無いと例外となります。
ごもっともなのですが、サーバー側がミスなり仕様ズレなどにより、場合によっては"a2"を返さないといった場合、例外となりデータが取得できません。
重要な値ならともかく、あってもなくてもよい値などの場合はデフォルト値などで埋めておいて欲しいです。 サーバーを信用するのがそもそも問題なのですが、
通信の基本は「送信側は厳格に、受信側は寛容に」
ですから、受信側は多少の問題も吸収するようになっているほうがよいです。

型制限が厳しい

型に合わないデータがあると例外になります。

{"a1":"1", "a2":"true"}

struct AA :Codable {
	var a1: Int
	var a2: Bool
}

この場合、Intに対してStringが来ているので例外となります。
こうゆうのよくありますよね。もちろん送信側が悪いのですが、喧嘩しても仕方ありません。
受信側は寛容にですので、Intなら可能な限りIntに、Boolなら可能な限りBoolにと落としてもらえる方がありがたいです。

構造体中にAnyが置けない

JSON中に共通部分と個別部分がある場合、とりあえず共通部部分だけ構造体に落として、あとは個別で処理したい場合など。

{
	"meta: {"status":100, "message":"aaa"},
	"data: {...}
}

struct Result: Codable {
	struct Meta: Codable {
		var status: Int
		var message: String
	}
	var meta: Meta
	var data: Any
}

とりあえずmetaをチェック、状況に従いdataを変換などといった場合、これが出来ない。まるごと構造体を書いて落とすのもちょっとしんどい。
AnyにCodableが出来ないので仕方ないですね。

enumで該当外の値の場合例外

変換できないから例外に行くのは仕方ないですけれどね。

いろいろ例外

変換で受付する形式にそぐわない状況など例外に落ちます。
それはとても正解なのですが、寛容さがないと現場では辛いです。

JSONDecoderについて

変換上の問題は、Decoderの

init(from: Decoder){}

を実装しまくればいくらか回避できますが、個別に書いていては面倒で折角の機能が台無しです。
とっても時間をかけてとっても冗長なコードを書くのが仕事だと言わんばかりの人は結構居ますが(小言になりそうなので略)、
コードは短く、作業は楽に、動作は安全に倒す方向で行きたいものです。

さて、JSONDecoder、これはprotocol Decoderを実装してみた1つに過ぎません。
swiftのリポジトリのコードはこちら

少々辛いコードですが、リリースされているものなので仕方ありません。
動作を把握しつつそっ閉じして、要するにDecoderを満たすものがあればよいのです。

public protocol Decoder {
	public var codingPath: [CodingKey] { get }
	public var userInfo: [CodingUserInfoKey : Any] { get }
	public func container<Key>(keyedBy type: Key.Type) throws -> KeyedDecodingContainer<Key> where Key : CodingKey
	public func unkeyedContainer() throws -> UnkeyedDecodingContainer
	public func singlevarueContainer() throws -> SinglevarueDecodingContainer
}

これならなんとかなりそうな気分になってきます。
単値・array・dictionaryなりに処理を満たせば、init(from: Decoder)で初期化されていくという、ややこしいですが割とうまいこと考えてある方法です。

ObjDecoder

それじゃあ実装し直して問題点も解決していこうというので試しに書いてみたのがこちら
です。

これもまた実装例の1つですし、最適な解というのは状況によりますので、podなりのライブラリにはしていません。
Decoderの実装は200行ちょいなので、見ればわかるかなと。

基本的な使い方はJSONDecoderと同じです。

let r = try? ObjDecoder().decode(A.self, from: a)

fromの値は、Dataだけでなく、jsonの文字列のままのString、それでもないときはオブジェクトとして、受付の幅を大きくしています。

let str = "{\"a\":1}"
let r = try? ObjDecoder().decode(A.self, from: str)

変換は寛容に、Int受付なら文字列であろうと可能な限り数値に変換、どうしようもない時はデフォルト値(数値:0, Bool:false, 文字列:"")に落としています。

こういうのでも通ります。

{"a":1, "b":"true", "d":"123", "e":{"f":"f", "g":"g"}}

struct A4: Codable {
	var a: Bool
	var b: Bool
	var c: Bool
	var d: Int
	struct E: Codable{
		var f:String
		var g:String
	}
	var e: E
}

let r = try! ObjDecoder().decode(A4.self, from: "{\"a\":1, \"b\":\"true\",\"d\":\"123\",\"e\":{\"f\":\"f\",\"g\":\"g\"}}")
print(r)
A4(a: true, b: true, c: false, d: 123, e: deco.A4.E(f: "f", g: "g"))

構造体中にAnyが置けない問題として、AnyCodableといったものをこしらえてあります。
Codable準拠としての処理をしていますが、ObjDecoderで通すと変換をスルーして処理します。
スルーしないと末端まで展開して格納するという無駄な処理が走ってしまいます。

{"a":1, "b":{"c:":2,"d":3}}

struct A: Codable {
	var a: Int
	var b: AnyCodable
}

struct B: Codable {
	var c: Int
	var d: Int
}

let a = try? ObjDecoder().decode(A.self, from: json) 
// ... なんか処理してunwrap
let b = try? ObjDecoder().decode(B.self, from: a.b)

という風に行けます。

Dataは文字列ならbase64、
Dateは文字列ならISO8601、数値ならunixtimeなどに対応。

snakecaseなどのKeyDecodingStrategy、日付フォーマットのDateDecodingStrategyなどは長くなるので入れていません。
そのかわり逃げ道のクロージャを設けておきました。
dictionaryの変換において、構造体のメンバーであるキーパスとデータを渡すので、都合よい形にしてAnyで返してくれたら後はなんとかするね、というものです。

typealias ObjDecoderConverter = ((_ path: [CodingKey], _ container: [String: Any]) -> Any?)

どこに置くも自由ですが、structに置いたりして

struct A: Codable {
    var isSuccess: Bool
    
	static func converter(path: [CodingKey], container: [String: Any]) -> Any? {
		if path.last?.stringValue == "isSuccess" { return container["is_success"]}
		return nil
	}
}

let r = try! ObjDecoder(converter: A.converter).decode(A.self, from: "{\"is_success\":true}")
print(r)
A(isSuccess: true)

面倒が増えてくるのでそこまですることはないと思いますが。
あといくつか仕掛けはありますが、読みつつ好きに弄っていけばと。

enumはどうする

enumで該当外の値がきた時は、なんらかの値に落とさねばなりません。

optionalで逃げられそうで逃げられないし、associated valueもCodableにはし辛い。
素直にデフォルト値設けてイニシャライザ書きますかね。

とにかくgenericに書けて、例外も丸め込んで出来るだけ短くしてこんなところ(optionalが二重に掛かってたりして)。
なんかいい手はないものですかね。

extension Decoder {
	func tes<T:RawRepresentable>(_ def:T) throws -> T where T.RawValue == String {
		return try T.init(rawValue: singleValueContainer().decode(String.self)) ?? def
	}

	func es<T:RawRepresentable>(_ def:T)  -> T where T.RawValue == String {
		return (try? tes(def)) ?? def
	}
}


enum A: String, Codable {
    case aaa, bbb
	case none; init(from: Decoder) { self = from.es(.none) }
}

Codableの値は何で宣言

sturct/classはどちらでもよいとして、var/let, optional, 初期値、はてさてDecodeで値がある時無い時どれがどれになるのか。

ざくっと試す

struct A: Codable {
	let a1: Int
	var a2: Int
	let a3: Int?
	var a4: Int?
	let a5: Int = 1
	var a6: Int = 1
	let a7: Int? = 1
	var a8: Int? = 1
}

あるとき

let r = try! ObjDecoder().decode(A.self, from: "{\"a1\":551,\"a2\":551,\"a3\":551,\"a4\":551,\"a5\":551,\"a6\":551,\"a7\":551,\"a8\":551}")
print(r)

A(a1: 551, a2: 551, a3: Optional(551), a4: Optional(551), a5: 1, a6: 551, a7: Optional(1), a8: Optional(551))

ないとき

let n = try! ObjDecoder().decode(A.self, from: "{}")
print(n)

A(a1: 0, a2: 0, a3: nil, a4: nil, a5: 1, a6: 0, a7: Optional(1), a8: nil)

a5とa7は値があるときも1のままで変換されて来ないのでまずいですね。つまりletで初期値付きは絶対ダメ。

というか初期値つけても意味がない。別の初期化などの事情でどうして初期値を書きたい場合はvarで。

optionalは無い時にnilになっているのでデフォルトに落ちるのが嫌な場合は有効。

安全牌としては var xx:XX? なところですが、
後でoptional処理が面倒なら var xx:XX あたり、
後で値いじられたくない場合はletですが、初期値はつけないようにと。

30
22
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
30
22

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?