Edited at

Codableで色々なJSONに対応する

元の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)!)


ルートがArrayのJSON.swift

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型のフォーマット

JSONDecoderdateDecodingStrategyプロパティ, JSONEncoderdateEncodingStrategyプロパティで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から、上記のようなスネークケース、キャメルケース変換は、JSONDecoderJSONEncoderのプロパティを使うことでも対応できます。

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