元のJSONの構造のまま利用できればいいけど、構造を変えようと思うと結構コード量が増えてくる。
WebAPIのレスポンスを利用するだけならDecodable
に準拠するだけで十分だと思いました。
サンプルコードはすべてPlaygroundで実行できます。
関連記事を書きました
基本
Codableに準拠していて、プロパティに使える型
Bool
, Int
, Int8
, Int16
, Int32
, Int64
, UInt
, UInt8
, UInt16
, UInt32
, UInt64
, Int
, Double
, String
あとは上記の型を要素に持つOptional
, Array
, Dictionary
let data = """
{
"model": "iPhone X",
"displaySize": 5.8,
"capacities": [64, 256],
"biometricsAuth": "Face ID"
}
""".data(using: .utf8)!
struct Device: Codable {
var model: String
var displaySize: Float
var capacities: [Int]
var biometricsAuth: String? // nullの場合がある or キーがない場合がある
}
let device = try? JSONDecoder().decode(Device.self, from: data)
let encoder = JSONEncoder()
encoder.outputFormatting = .prettyPrinted // リーダブルな出力
let encoded = try! encoder.encode(device)
print(String(data: encoded, encoding: .utf8)!)
let list = """
[
{
"model": "iPhone 3G",
"displaySize": 3.5,
"capacities": [8, 16],
"biometricsAuth": null
},
{
"model": "iPhone 4",
"displaySize": 3.5,
"capacities": [8, 16, 32]
}
]
""".data(using: .utf8)!
let devices = try? JSONDecoder().decode([Device].self, from: list)
let encoder = JSONEncoder()
encoder.outputFormatting = .prettyPrinted
let encoded = try! encoder.encode(devices)
print(String(data: encoded, encoding: .utf8)!)
値にenumを使ったり、structをネストさせる
enumを利用する場合、RawRepresentable
に準拠していてRawValue
が
Bool
,Int
,Int8
,Int16
,Int32
,Int64
,UInt
,UInt8
,UInt16
,UInt32
,UInt64
,Int
,Double
,String
であれば、init(from:), encode(to:) のデフォルト実装が用意されてるので簡単。
let data = """
{
"model": "iPhone X",
"capacities": [64, 256],
"size": {
"height": 143,
"width": 70,
"depth": 7
}
}
""".data(using: .utf8)!
struct Device: Codable {
var model: Model
var capacities: [Capacity]
var size: Size
enum Model: String, Codable {
case iPhoneX = "iPhone X"
case iPhone8 = "iPhone 8"
case iPhone8Plus = "iPhone 8 Plus"
}
enum Capacity: Int, Codable {
case _64 = 64
case _256 = 256
}
struct Size: Codable {
var height: Int
var width: Int
var depth: Int
}
}
let device = try? JSONDecoder().decode(Device.self, from: data)
let encoder = JSONEncoder()
encoder.outputFormatting = .prettyPrinted
let encoded = try! encoder.encode(device)
print(String(data: encoded, encoding: .utf8)!)
enumのassociated valueを利用する
いずれかのキーでレスポンスが返ってくるみたいな場合に
// json1 か json2 どちらかの形式でレスポンスが返る想定
let jsonData: Data = {
let json1 = """
{"str": "文字列"}
"""
let json2 = """
{"num": 777}
"""
return [json1, json2]
.randomElement()!
.data(using: .utf8)!
}()
enum Response: Codable {
case str(String)
case num(Int)
private enum CodingKeys: String, CodingKey, CaseIterable {
case str
case num
}
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
if let value = try container.decodeIfPresent(String.self, forKey: .str) {
self = .str(value)
} else if let value = try container.decodeIfPresent(Int.self, forKey: .num) {
self = .num(value)
} else {
throw DecodingError.dataCorrupted(.init(codingPath: CodingKeys.allCases,
debugDescription: "Does not match any CodingKey."))
}
}
func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
switch self {
case .str(let value):
try container.encode(value, forKey: .str)
case .num(let value):
try container.encode(value, forKey: .num)
}
}
}
let decoder = JSONDecoder()
let encoder = JSONEncoder()
encoder.outputFormatting = .prettyPrinted
do {
let response = try decoder.decode(Response.self, from: jsonData)
let encoded = try encoder.encode(response)
print(String(data: encoded, encoding: .utf8)!)
} catch {
dump(error)
}
Date型のフォーマット
JSONDecoder
はdateDecodingStrategy
プロパティ, JSONEncoder
はdateEncodingStrategy
プロパティでDate型のパース方法を指定できる。
let data = """
{
"model": "iPhone X",
"releaseDate": "2017-10-19T11:53:36Z"
}
""".data(using: .utf8)!
struct Device: Codable {
var model: String
var releaseDate: Date
}
let decoder = JSONDecoder()
decoder.dateDecodingStrategy = .iso8601
let device = try? decoder.decode(Device.self, from: data)
let encoder = JSONEncoder()
encoder.outputFormatting = .prettyPrinted
encoder.dateEncodingStrategy = .iso8601
let encoded = try! encoder.encode(device)
print(String(data: encoded, encoding: .utf8)!)
型変換
JSONで数値の文字列"123"
が返ってくるが、SwiftではInt
で扱いたい場合など
let data = """
{
"model": "iPhone X",
"capacity": "64"
}
""".data(using: .utf8)!
struct Device: Codable {
var model: String
var capacity: Int
private enum CodingKeys: String, CodingKey {
case model
case capacity
}
init(from decoder: Decoder) throws {
let values = try decoder.container(keyedBy: CodingKeys.self)
model = try values.decode(String.self, forKey: .model)
capacity = Int(try values.decode(String.self, forKey: .capacity)) ?? 0
}
func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(model, forKey: .model)
try container.encode(capacity.description, forKey: .capacity)
}
}
let device = try? JSONDecoder().decode(Device.self, from: data)
let encoder = JSONEncoder()
encoder.outputFormatting = .prettyPrinted
let encoded = try! encoder.encode(device)
print(String(data: encoded, encoding: .utf8)!)
JSONのkeyと、structのpropertyのマッピング
let data = """
{
"model_name": "iPhone X"
}
""".data(using: .utf8)!
struct Device: Codable {
var modelName: String
private enum CodingKeys: String, CodingKey {
case modelName = "model_name"
}
}
let device = try? JSONDecoder().decode(Device.self, from: data)
let encoder = JSONEncoder()
encoder.outputFormatting = .prettyPrinted
let encoded = try! encoder.encode(device)
print(String(data: encoded, encoding: .utf8)!)
Swift 4.1から、上記のようなスネークケース、キャメルケース変換は、JSONDecoder
、JSONEncoder
のプロパティを使うことでも対応できます。
let data = """
{
"model_name": "iPhone X"
}
""".data(using: .utf8)!
struct Device: Codable {
var modelName: String
}
let decoder = JSONDecoder()
decoder.keyDecodingStrategy = .convertFromSnakeCase // スネークケースからの変換指定
let device = try? decoder.decode(Device.self, from: data)
let encoder = JSONEncoder()
encoder.outputFormatting = .prettyPrinted
encoder.keyEncodingStrategy = .convertToSnakeCase // スネークケースへの変換指定
let encoded = try! encoder.encode(device)
print(String(data: encoded, encoding: .utf8)!)
ネストしたJSON <=> Flatな構造体
let data = """
{
"model": "iPhone X",
"specs": {
"color": "Space Gray",
"capacity": 64
}
}
""".data(using: .utf8)!
struct Device: Codable {
var model: String
var color: Color
var capacity: Int
enum Color: String, Codable {
case spaceGray = "Space Gray"
case silver = "Silver"
}
private enum CodingKeys: String, CodingKey {
case model
case specs
}
private enum SpecsKeys: String, CodingKey {
case color
case capacity
}
init(from decoder: Decoder) throws {
let values = try decoder.container(keyedBy: CodingKeys.self)
model = try values.decode(String.self, forKey: .model)
let specs = try values.nestedContainer(keyedBy: SpecsKeys.self, forKey: .specs)
color = try specs.decode(Color.self, forKey: .color)
capacity = try specs.decode(Int.self, forKey: .capacity)
}
func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(model, forKey: .model)
var specs = container.nestedContainer(keyedBy: SpecsKeys.self, forKey: .specs)
try specs.encode(color, forKey: .color)
try specs.encode(capacity, forKey: .capacity)
}
}
let device = try? JSONDecoder().decode(Device.self, from: data)
let encoder = JSONEncoder()
encoder.outputFormatting = .prettyPrinted
let encoded = try! encoder.encode(device)
print(String(data: encoded, encoding: .utf8)!)
FlatなJSON <=> ネストした構造体
import Foundation
let data = """
{
"model": "iPhone X",
"height": 143,
"width": 70,
"depth": 7
}
""".data(using: .utf8)!
struct Device: Codable {
var model: String
var size: Size
struct Size: Codable {
var height: Int
var width: Int
var depth: Int
}
private enum CodingKeys: String, CodingKey {
case model
case height
case width
case depth
}
init(from decoder: Decoder) throws {
let values = try decoder.container(keyedBy: CodingKeys.self)
model = try values.decode(String.self, forKey: .model)
size = try Size(from: decoder)
}
func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(model, forKey: .model)
try size.encode(to: encoder)
}
}
let decoder = JSONDecoder()
let encoder = JSONEncoder()
encoder.outputFormatting = .prettyPrinted
do {
let device = try JSONDecoder().decode(Device.self, from: data)
dump(device)
let encoded = try encoder.encode(device)
print(String(data: encoded, encoding: .utf8)!)
} catch {
dump(error)
}
RawRepresentableを利用して苦しいJSONに対応
サイズの範囲情報が _
区切りで表現されているJSONにどう対応するかを考えてみます。
{
"size": "55.0_61.5"
}
Swiftの世界ではこのように扱いたいですね。
struct Size {
var min: Double
var max: Double
}
RawRepresentableを利用します。
let json = """
{
"size": "55.0_61.5"
}
""".data(using: .utf8)!
struct Parameter: Codable {
var size: Size
}
struct Size: Codable {
var min: Double
var max: Double
}
extension Size: RawRepresentable {
init?(rawValue: String) {
let separated = rawValue.components(separatedBy: "_")
guard
separated.count == 2,
let min = separated.first.flatMap(Double.init),
let max = separated.last.flatMap(Double.init)
else { return nil }
self.init(min: min, max: max)
}
var rawValue: String {
return [String(min), String(max)].joined(separator: "_")
}
}
let decoder = JSONDecoder()
let decoded = try! decoder.decode(Parameter.self, from: json)
dump(decoded)
// __lldb_expr_11.Parameter
// size: __lldb_expr_11.Size
// - min: 55.0
// - max: 61.5
let encoder = JSONEncoder()
let encoded = try! encoder.encode(decoded)
print(String(data: encoded, encoding: .utf8)!)
// {"size":"55.0_61.5"}
参考
Codable - Swift Standard Library | Apple Developer Documentation
Swift4のJSONDecorderは、Date等のパース方法をカスタマイズできるみたい - Qiita
Codable in Swift 4.0 – Sarun W. – Medium