Swift
swift4
Codable

Codableで色々なJSONに対応する

More than 1 year has passed since last update.

元の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を利用する

いずれかのキーでレスポンスが返ってくるみたいな場合に

// data1 か data2 どちらかの形式でレスポンスが返る想定
let data1 = """
{"str": "文字列"}
""".data(using: .utf8)!

let data2 = """
{"num": 777}
""".data(using: .utf8)!


enum Response: Codable {
    case str(String)
    case num(Int)

    private enum CodingKeys: String, CodingKey {
        case str
        case num
    }

    enum CodingError: Error {
        case decoding(String)
    }

    init(from decoder: Decoder) throws {
        let values = try decoder.container(keyedBy: CodingKeys.self)

        if let value = try? values.decode(String.self, forKey: .str) {
            self = .str(value)
            return
        }

        if let value = try? values.decode(Int.self, forKey: .num) {
            self = .num(value)
            return
        }

        throw CodingError.decoding("\(dump(values))")
    }

    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 str = try? JSONDecoder().decode(Response.self, from: data1)
let num = try? JSONDecoder().decode(Response.self, from: data2)

let encoder = JSONEncoder()
encoder.outputFormatting = .prettyPrinted
let encodedStr = try! encoder.encode(str)
print(String(data: encodedStr, encoding: .utf8)!)

let encodedNum = try! encoder.encode(num)
print(String(data: encodedNum, encoding: .utf8)!)

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 model: String

    private enum CodingKeys: String, CodingKey {
        case model = "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)!)

ネストした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 <=> ネストした構造体

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 container.encode(size.height, forKey: .height)
        try container.encode(size.width, forKey: .width)
        try container.encode(size.depth, forKey: .depth)
    }
}


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

参考

Codable - Swift Standard Library | Apple Developer Documentation
Swift4のJSONDecorderは、Date等のパース方法をカスタマイズできるみたい - Qiita
Codable in Swift 4.0 – Sarun W. – Medium