web-dev-qa-db-ja.com

JSONEncoderでnil値をnullとしてエンコードする

私はSwift 4のJSONEncoderを使用しています。オプションのプロパティを持つCodable構造体があり、値がnullの場合、このプロパティを生成されたJSONデータのnil値として表示したいです)ただし、JSONEncoderはプロパティを破棄し、JSON出力に追加しません。この場合、JSONEncoderを設定してキーを保持し、nullに設定する方法はありますか?

以下のコードスニペットは{"number":1}を生成しますが、{"string":null,"number":1}を提供したいです:

struct Foo: Codable {
  var string: String? = nil
  var number: Int = 1
}

let encoder = JSONEncoder()
let data = try! encoder.encode(Foo())
print(String(data: data, encoding: .utf8)!)
35
dr_barto

はい。ただし、独自のencode(to:)実装を作成する必要があります。自動生成された実装は使用できません。

struct Foo: Codable {
    var string: String? = nil
    var number: Int = 1

    func encode(to encoder: Encoder) throws {
        var container = encoder.container(keyedBy: CodingKeys.self)
        try container.encode(number, forKey: .number)
        try container.encode(string, forKey: .string)
    }
}

オプションを直接エンコードすると、お探しのようにnullがエンコードされます。

これが重要なユースケースである場合は、 bugs.Swift.org で欠陥を開いて、既存と一致するJSONEncoderに追加する新しいOptionalEncodingStrategyフラグを要求することを検討できます。 DateEncodingStrategyなど(以下を参照してください。実際にSwiftで実際に実装することが不可能である可能性が高いのですが、追跡システムに入ることはSwift進化します。)


編集:以下のPauloの質問に、これはOptionalEncodableに準拠しているため、汎用encode<T: Encodable>バージョンにディスパッチします。これは Codable.Swift で次のように実装されます。

extension Optional : Encodable /* where Wrapped : Encodable */ {
    @_inlineable // FIXME(sil-serialize-all)
    public func encode(to encoder: Encoder) throws {
        assertTypeIsEncodable(Wrapped.self, in: type(of: self))

        var container = encoder.singleValueContainer()
        switch self {
        case .none: try container.encodeNil()
        case .some(let wrapped): try (wrapped as! Encodable).__encode(to: &container)
        }
    }
}

これはencodeNilの呼び出しをラップし、stdlibがOptionalを別のEncodableとして処理できるようにすることは、独自のエンコーダで特殊なケースとして扱い、encodeNilを呼び出すよりも優れていると思います。

別の明らかな質問は、そもそもなぜこのように機能するのかということです。 OptionalはEncodableであり、生成されたEncodable準拠はすべてのプロパティをエンコードするので、「すべてのプロパティを手動でエンコードする」のはなぜ機能しないのですか?答えは、適合ジェネレーター オプションの特殊なケースを含む

// Now need to generate `try container.encode(x, forKey: .x)` for all
// existing properties. Optional properties get `encodeIfPresent`.
...

if (varType->getAnyNominal() == C.getOptionalDecl() ||
    varType->getAnyNominal() == C.getImplicitlyUnwrappedOptionalDecl()) {
  methodName = C.Id_encodeIfPresent;
}

つまり、この動作を変更するには、JSONEncoderではなく、自動生成された適合性を変更する必要があります(つまり、今日のSwiftで設定可能にするのはおそらく非常に難しいことも意味します)。

29
Rob Napier

私は同じ問題に遭遇しました。 JSONEncoderを使用せずに構造体から辞書を作成することで解決しました。これは、比較的普遍的な方法で実行できます。私のコードは次のとおりです。

struct MyStruct: Codable {
    let id: String
    let regionsID: Int?
    let created: Int
    let modified: Int
    let removed: Int?


    enum CodingKeys: String, CodingKey, CaseIterable {
        case id = "id"
        case regionsID = "regions_id"
        case created = "created"
        case modified = "modified"
        case removed = "removed"
    }

    var jsonDictionary: [String : Any] {
        let mirror = Mirror(reflecting: self)
        var dic = [String: Any]()
        var counter = 0
        for (name, value) in mirror.children {
            let key = CodingKeys.allCases[counter]
            dic[key.stringValue] = value
            counter += 1
        }
        return dic
    }
}

extension Array where Element == MyStruct {
    func jsonArray() -> [[String: Any]] {
        var array = [[String:Any]]()
        for element in self {
            array.append(element.jsonDictionary)
        }
        return array
    }
}

CodingKeysなしでこれを行うことができます(サーバー側のテーブル属性名が構造プロパティ名と等しい場合)。その場合は、mirror.childrenの「名前」を使用してください。

CodingKeysが必要な場合は、CaseIterableプロトコルを追加することを忘れないでください。これにより、allCases変数を使用できます。

ネストされた構造体には注意してください。型としてカスタム構造を持つプロパティがある場合は、それも辞書に変換する必要があります。これはforループで実行できます。

MyStruct辞書の配列を作成する場合は、Array拡張機能が必要です。

0
guido