Skip to content

Instantly share code, notes, and snippets.

@takasek
Last active July 18, 2020 09:39
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save takasek/d805b0e1a0bef7746172fc609995ed88 to your computer and use it in GitHub Desktop.
Save takasek/d805b0e1a0bef7746172fc609995ed88 to your computer and use it in GitHub Desktop.
「派生型プロパティを Decodable で扱う」 https://hitorigoto.zumuya.com/200716_decodableProtocolProperty にインスパイヤされたコード。いじってたらPropertyWrapperがなくなってしまった…
// 汎用コード
struct CustomCodingKey: CodingKey, ExpressibleByStringLiteral {
let stringValue: String
let intValue: Int? = nil
init?(stringValue: String) { self.stringValue = stringValue }
init?(intValue: Int) { return nil }
init(stringLiteral value: String) { stringValue = value }
init(_ value: String) { stringValue = value }
}
protocol PolymorphicDecodable: Decodable {
static var hintKeyName: String { get }
init?(hint: String, decoder: Decoder, container: KeyedDecodingContainer<CustomCodingKey>) throws
}
extension PolymorphicDecodable {
static var hintKeyName: String { "type" }
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CustomCodingKey.self)
let hintKey = CustomCodingKey(Self.hintKeyName)
let hint = try container.decode(String.self, forKey: hintKey)
guard let v = try Self(hint: hint, decoder: decoder, container: container) else {
throw DecodingError.dataCorruptedError(
forKey: hintKey,
in: container,
debugDescription: "unknown type hint: \(hint)"
)
}
self = v
}
}
// 以下 ドメイン固有コード
struct BoolParameter: Decodable {
let defaultValue: Bool
}
struct NumberParameter: Decodable {
let defaultValue: CGFloat
let minValue: CGFloat
let maxValue: CGFloat
}
enum BoolOrNumberParameter: PolymorphicDecodable {
case bool(BoolParameter)
case number(NumberParameter)
init?(hint: String, decoder: Decoder, container: KeyedDecodingContainer<CustomCodingKey>) throws {
switch hint {
case "bool": self = .bool(try BoolParameter(from: decoder))
case "number": self = .number(try NumberParameter(from: decoder))
default: return nil
}
}
}
struct Bird: Decodable {
let eggs: [String]
let destination: URL
}
enum Animal: PolymorphicDecodable {
case dog(name: String)
case cat(name: String, cuteFactor: Double)
case bird(Bird)
static var hintKeyName: String { "kind" }
init?(hint: String, decoder: Decoder, container: KeyedDecodingContainer<CustomCodingKey>) throws {
switch hint {
case "dog": self = .dog(name: try container.decode(String.self, forKey: "name"))
case "cat": self = .cat(name: try container.decode(String.self, forKey: "name"),
cuteFactor: try container.decode(Double.self, forKey: "cute_factor"))
case "bird": self = .bird(try Bird(from: decoder))
default: return nil
}
}
}
struct Root: Decodable {
let parameter: BoolOrNumberParameter
let pets: [Animal]
}
let root = try JSONDecoder().decode(Root.self, from: """
{
"parameter": {
"type": "number",
"defaultValue": 1.5,
"minValue": 0.0,
"maxValue": 2.0
},
"pets": [
{ "kind": "dog", "name": "pochi" },
{ "kind": "cat", "name": "tama", "cute_factor": 99.99 },
{ "kind": "bird", "eggs": [ "uzura", "温泉", "ホビロン" ], "destination": "https://www.youtube.com/watch?v=8kQZHYbZkLs" }
]
}
""".data(using: .utf8)!)
dump(root)
//▿ __lldb_expr_89.Root
//▿ parameter: __lldb_expr_89.BoolOrNumberParameter.number
// ▿ number: __lldb_expr_89.NumberParameter
// - defaultValue: 1.5
// - minValue: 0.0
// - maxValue: 2.0
//▿ pets: 3 elements
// ▿ __lldb_expr_89.Animal.dog
// ▿ dog: (1 element)
// - name: "pochi"
// ▿ __lldb_expr_89.Animal.cat
// ▿ cat: (2 elements)
// - name: "tama"
// - cuteFactor: 99.99
// ▿ __lldb_expr_89.Animal.bird
// ▿ bird: __lldb_expr_89.Bird
// ▿ eggs: 3 elements
// - "uzura"
// - "温泉"
// - "ホビロン"
// ▿ destination: https://www.youtube.com/watch?v=8kQZHYbZkLs
// - _url: https://www.youtube.com/watch?v=8kQZHYbZkLs #0
// - super: NSObject
@takasek
Copy link
Author

takasek commented Jul 17, 2020

元コードから変わった点

  • 型情報を失いたくないのでデータの派生はenumで表現
  • CustomDecodedParameter をデータごとにいちいち作らなくていいようドメイン固有の処理を切り出し、共通部分をprotocol extensionに
  • typeが想定外だったときにfatalErrorだと失敗時にアプリごと死ぬので、適切にCodingErrorをthrow
  • 単体でもArrayでも同じように扱えるように工夫していたらPropertyWrapperが消滅してしまいました…

@takasek
Copy link
Author

takasek commented Jul 18, 2020

※ なおJSONはあくまで例であり、

  • 情報に意味はありません
  • そもそもこういうレスポンス設計はおすすめしません
{
    "pets": [
        { "kind": "dog", "name": "pochi" },
        { "kind": "cat", "name": "tama", "cute_factor": 99.99 },
        { "kind": "bird", "eggs": [ "uzura", "温泉", "ホビロン" ], "destination": "https://www.youtube.com/watch?v=8kQZHYbZkLs" }
    ]
}

ではなく

{
    "pets": [
        { "kind": "dog", "dog": { "name": "pochi" } },
        { "kind": "cat", "cat": { "name": "tama", "cute_factor": 99.99 } },
        { "kind": "bird", "bird": { "eggs": [ "uzura", "温泉", "ホビロン" ], "destination": "https://www.youtube.com/watch?v=8kQZHYbZkLs" } }
    ]
}

のように、オブジェクト種別のヒントと具体的なデータはnestさせてオブジェクトごと切り分けることを強く勧めます

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment