简体   繁体   English

如何使用包含已编码值的 Encodable 对 Swift 中的结构进行编码

[英]How to encode struct in Swift using Encodable that contains an already encoded value

Imagine a data structure as follows, containing a value in contents that is an already encoded JSON fragment.想象一个如下的数据结构,在contents中包含一个值,该值是一个已经编码的 JSON 片段。

let partial = """
{ "foo": "Foo", "bar": 1 }
"""

struct Document {
  let contents: String
  let other: [String: Int]
}

let doc = Document(contents: partial, other: ["foo": 1])

Desired output所需 output

The combined data structure should use contents as is and encode other .组合数据结构应按原样使用contents并对其other编码。

{
  "contents": { "foo": "Foo", "bar": 1 },
  "other": { "foo": 1 }
}

Using Encodable使用Encodable

The following implementation of Encodable encodes Document as JSON, however it also re-encodes contents into a string, meaning it is wrapped in quotes and has all " quotes escaped into \" .下面的Encodable实现将Document编码为 JSON,但它也将contents重新编码为字符串,这意味着它被包裹在引号中,并且所有"引号都转义为\"

extension Document : Encodable {
    enum CodingKeys : String, CodingKey {
        case contents
        case other
    }

    func encode(to encoder: Encoder) throws {
        var container = encoder.container(keyedBy: CodingKeys.self)

        try container.encode(contents, forKey: .contents)
        try container.encode(other, forKey: .other)
    }
}

Output Output

{
  "contents": "{\"foo\": \"Foo\", \"bar\": 1}",
  "other": { "foo": 1 }
}

How can encode just pass through contents as is? encode如何按原样传递contents

I agree with Ahmad's basic approach, but I'm assuming you need something more dynamic.我同意艾哈迈德的基本方法,但我假设你需要一些更有活力的东西。 In that case, you should make clear that content is not a "String."在这种情况下,您应该明确content不是“字符串”。 It's JSON.它是 JSON。 And so you can store it as JSON using a JSON type (simplified here, see the gist for a more feature-rich version):因此,您可以使用JSON 类型将其存储为 JSON (在此处进行了简化,请参阅要点以获得更丰富的版本):

enum JSON: Codable {
    struct Key: CodingKey, Hashable, CustomStringConvertible {
        var description: String {
            return stringValue
        }

        let stringValue: String
        init(_ string: String) { self.stringValue = string }
        init?(stringValue: String) { self.init(stringValue) }
        var intValue: Int? { return nil }
        init?(intValue: Int) { return nil }
    }

    case string(String)
    case number(Double) // FIXME: Split Int and Double
    case object([Key: JSON])
    case array([JSON])
    case bool(Bool)
    case null

    init(from decoder: Decoder) throws {
        if let string = try? decoder.singleValueContainer().decode(String.self) { self = .string(string) }
        else if let number = try? decoder.singleValueContainer().decode(Double.self) { self = .number(number) }
        else if let object = try? decoder.container(keyedBy: Key.self) {
            var result: [Key: JSON] = [:]
            for key in object.allKeys {
                result[key] = (try? object.decode(JSON.self, forKey: key)) ?? .null
            }
            self = .object(result)
        }
        else if var array = try? decoder.unkeyedContainer() {
            var result: [JSON] = []
            for _ in 0..<(array.count ?? 0) {
                result.append(try array.decode(JSON.self))
            }
            self = .array(result)
        }
        else if let bool = try? decoder.singleValueContainer().decode(Bool.self) { self = .bool(bool) }
        else if let isNull = try? decoder.singleValueContainer().decodeNil(), isNull { self = .null }
        else { throw DecodingError.dataCorrupted(DecodingError.Context(codingPath: [],
                                                                       debugDescription: "Unknown JSON type")) }
    }

    func encode(to encoder: Encoder) throws {
        switch self {
        case .string(let string):
            var container = encoder.singleValueContainer()
            try container.encode(string)
        case .number(let number):
            var container = encoder.singleValueContainer()
            try container.encode(number)
        case .bool(let bool):
            var container = encoder.singleValueContainer()
            try container.encode(bool)
        case .object(let object):
            var container = encoder.container(keyedBy: Key.self)
            for (key, value) in object {
                try container.encode(value, forKey: key)
            }
        case .array(let array):
            var container = encoder.unkeyedContainer()
            for value in array {
                try container.encode(value)
            }
        case .null:
            var container = encoder.singleValueContainer()
            try container.encodeNil()
        }
    }
}

With that you can redefine your document to hold JSON:有了它,您可以重新定义您的文档以保存 JSON:

struct Document: Codable {
  let contents: JSON
  let other: [String: Int]
}

And decode that JSON from a String if you like:如果您愿意,可以从字符串中解码 JSON:

let doc = Document(contents:
    try! JSONDecoder().decode(JSON.self, from: Data(partial.utf8)),
                   other: ["foo": 1])

With that in place, the default JSONEncoder() is all you need to get the output you're describing.有了这个,默认的JSONEncoder()就是你所描述的 output 所需要的一切。

You could achieve it by doing this:你可以通过这样做来实现它:

let partial = """
{
"foo": "Foo",
"bar": 1
}
"""

// declare a new type for `content` to deal with it as an object instead of a string
struct Document {
    let contents: Contents
    let other: [String: Int]

    struct Contents: Codable {
        let foo: String
        let bar: Int
    }
}

extension Document : Encodable {
    enum CodingKeys: String, CodingKey {
        case contents
        case other
    }

    func encode(to encoder: Encoder) throws {
        var container = encoder.container(keyedBy: CodingKeys.self)

        try container.encode(contents, forKey: .contents)
        try container.encode(other, forKey: .other)
    }
}

let decoder = JSONDecoder()
let contents = try decoder.decode(Document.Contents.self, from: partial.data(using: .utf8)!)

let encoder = JSONEncoder()
let doc = Document(contents: contents, other: ["foo": 1])
let result = try encoder.encode(doc)
print(String(data: result, encoding: .utf8)!)

Basically, you could deal with partial first by decoding it, and then you pass the decoded result of it to Document .基本上,您可以首先通过对其进行解码来处理partial ,然后将其解码结果传递给Document

The output should be: output 应该是:

{"other":{"foo":1},"contents":{"foo":"Foo","bar":1}} {"other":{"foo":1},"contents":{"foo":"Foo","bar":1}}

I might be a bit late, but I hope it helps someone in the future.我可能有点晚了,但我希望它对未来的人有所帮助。 I've had a similar problem where I had some pre-encoded variables and wanted to nest them inside some parent structure that was encodable.我有一个类似的问题,我有一些预编码的变量,并想将它们嵌套在一些可编码的父结构中。

struct Request: Encodable {
    let variables: [String: Data] // I'd encode data to JSON someplace else.
}

Unfortunately, the type of each keyed value varied (eg you could have an integer in one key and an object in the other) and I couldn't pass information upwards from where I was encoding it the first time.不幸的是,每个键值的类型各不相同(例如,您可以在一个键中使用 integer,在另一个键中使用 object)并且我无法从第一次对其进行编码的位置向上传递信息。 Here's what I have in mind:这是我的想法:

{ 
    "variables": {
        "one": { "hello": "world" },
        "two": 2
    }
}

Enum and Generics were also not an option as this was a highly flexible part that only required types to conform to Encodable. Enum 和 Generics 也不是一个选项,因为这是一个高度灵活的部分,只需要符合 Encodable 的类型。

All in all, I ended up copying over most of Swift's JSONEncoder implementation that you can find here .总而言之,我最终复制了您可以在此处找到的大部分 Swift JSONEncoder 实现。 (I recommend cleaning out the JSONDecoder implementation since it's useless in our case.) (我建议清除 JSONDecoder 实现,因为它在我们的案例中没有用。)

The part that needs changing is inside the encode function in JSONEncoder class.需要更改的部分是 JSONEncoder class 中的encode JSONEncoder内部。 Basically, you want to split the parts that get topLevel value (ie NSObject ) and the part that serializes it.基本上,您想要拆分获得topLevel值的部分(即NSObject )和序列化它的部分。 The new encode should also return a NSObject -type instead of Data .新的encode还应该返回一个NSObject类型而不是Data

open func encode<T : Encodable>(_ value: T) throws -> NSObject {
    let encoder = __JSONEncoder(options: self.options)

    guard let topLevel = try encoder.box_(value) else {
        throw EncodingError.invalidValue(value, EncodingError.Context(codingPath: [], debugDescription: "Top-level \(T.self) did not encode any values."))
    }

    return topLevel
}

Once you have that, you may pass NSObject anywhere as a type, the important chunk left is that you run JSONSerialization.data function to get the actual JSON.一旦你有了它,你可以将NSObject作为类型传递到任何地方,剩下的重要块是你运行JSONSerialization.data function 来获得实际的 JSON。 What JSONEncoder does internally is that it reduces an Encodable struct to Foundation types. JSONEncoder 在内部所做的是将Encodable结构简化为Foundation类型。 JSONSerialization can then handle those types and you get a valid JSON. JSONSerialization然后可以处理这些类型,并且您会得到一个有效的 JSON。

Here's how I used it:这是我使用它的方式:

let body: Any = [
    "query": query, // String
    "variables": variables // NSObject dictionary
]

let httpBody = try! JSONSerialization.data(
    withJSONObject: body,
    options: JSONSerialization.WritingOptions()
)
request.httpBody = httpBody

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM