LoginSignup
233
187

More than 3 years have passed since last update.

Codableで色々なJSONに対応する

Last updated at Posted at 2017-10-20

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

233
187
0

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
233
187