web-dev-qa-db-ja.com

Swift Codableで部分的に動的なJSONを処理する方法は?

WebSocket接続を介して受信するJSONメッセージがいくつかあります。

// sample message
{
  type: "person",
  data: {
    name: "john"
  }
}

// some other message
{
  type: "location",
  data: {
    x: 101,
    y: 56
  }
}

Swift 4とCodableプロトコルを使用して、これらのメッセージを適切な構造体に変換するにはどうすればよいですか?

Goでは、次のようなことができます。「現時点では、typeフィールドのみが気になり、残り(dataの部分)には関心がありません。」このようになります

type Message struct {
  Type string `json:"type"`
  Data json.RawMessage `json:"data"`
}

ご覧のとおり、Dataのタイプはjson.RawMessageこれは後で解析できます。これが完全な例です https://golang.org/pkg/encoding/json/#example_RawMessage_unmarshal

Swiftで同様のことを行うことはできますか?のように(まだ試していません)

struct Message: Codable {
  var type: String
  var data: [String: Any]
}

次に、switchtypeを実行して、辞書を適切な構造体に変換します。それはうまくいくでしょうか?

14
zemirco

私はDictionaryに依存しません。カスタムタイプを使用します。

たとえば、次のように仮定します。

  • どのオブジェクトを取得するかがわかっています(リクエストの性質上)。そして

  • 2つのタイプの応答は、dataの内容を除いて真に同一の構造を返します。

その場合、非常に単純な汎用パターンを使用できます。

_struct Person: Decodable {
    let name: String
}

struct Location: Decodable {
    let x: Int
    let y: Int
}

struct ServerResponse<T: Decodable>: Decodable {
    let type: String
    let data: T
}
_

そして、Personを使用して応答を解析する場合、次のようになります。

_let data = json.data(using: .utf8)!
do {
    let responseObject = try JSONDecoder().decode(ServerResponse<Person>.self, from: data)

    let person = responseObject.data
    print(person)
} catch let parseError {
    print(parseError)
}
_

または、Locationを解析するには:

_do {
    let responseObject = try JSONDecoder().decode(ServerResponse<Location>.self, from: data)

    let location = responseObject.data
    print(location)
} catch let parseError {
    print(parseError)
}
_

楽しませることができるより複雑なパターンがあります(たとえば、遭遇したdata値に基づいたtype型の動的解析)が、必要でない限り、そのようなパターンを追求する傾向はありません。これは、特定のリクエストに関連付けられた応答タイプがわかっている典型的なパターンを実現する、優れたシンプルなアプローチです。


必要に応じて、type値から解析された値でdata値を検証できます。考えてみましょう:

_enum PayloadType: String, Decodable {
    case person = "person"
    case location = "location"
}

protocol Payload: Decodable {
    static var payloadType: PayloadType { get }
}

struct Person: Payload {
    let name: String
    static let payloadType = PayloadType.person
}

struct Location: Payload {
    let x: Int
    let y: Int
    static let payloadType = PayloadType.location
}

struct ServerResponse<T: Payload>: Decodable {
    let type: PayloadType
    let data: T
}
_

次に、parse関数は正しいdata構造を解析できるだけでなく、type値を確認できます。例:

_enum ParseError: Error {
    case wrongPayloadType
}

func parse<T: Payload>(_ data: Data) throws -> T {
    let responseObject = try JSONDecoder().decode(ServerResponse<T>.self, from: data)

    guard responseObject.type == T.payloadType else {
        throw ParseError.wrongPayloadType
    }

    return responseObject.data
}
_

そして、あなたはそれをそのように呼ぶことができます:

_do {
    let location: Location = try parse(data)
    print(location)
} catch let parseError {
    print(parseError)
}
_

これは、Locationオブジェクトを返すだけでなく、サーバー応答のtypeの値も検証します。努力する価値があるかどうかはわかりませんが、そうしたい場合は、それがアプローチです。


JSONを処理するときにタイプが本当にわからない場合は、最初にtypeを解析し、次にdataを解析するinit(coder:)を記述する必要があります。 typeに含まれる値に基づいて:

_enum PayloadType: String, Decodable {
    case person = "person"
    case location = "location"
}

protocol Payload: Decodable {
    static var payloadType: PayloadType { get }
}

struct Person: Payload {
    let name: String
    static let payloadType = PayloadType.person
}

struct Location: Payload {
    let x: Int
    let y: Int
    static let payloadType = PayloadType.location
}

struct ServerResponse: Decodable {
    let type: PayloadType
    let data: Payload

    init(from decoder: Decoder) throws {
        let values = try decoder.container(keyedBy: CodingKeys.self)
        type = try values.decode(PayloadType.self, forKey: .type)
        switch type {
        case .person:
            data = try values.decode(Person.self, forKey: .data)
        case .location:
            data = try values.decode(Location.self, forKey: .data)
        }
    }

    enum CodingKeys: String, CodingKey {
        case type, data
    }

}
_

そして、次のようなことができます。

_do {
    let responseObject = try JSONDecoder().decode(ServerResponse.self, from: data)
    let payload = responseObject.data
    if payload is Location {
        print("location:", payload)
    } else if payload is Person {
        print("person:", payload)
    }
} catch let parseError {
    print(parseError)
}
_
13
Rob