[英]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])
The combined data structure should use contents
as is and encode other
.组合数据结构应按原样使用
contents
并对其other
编码。
{
"contents": { "foo": "Foo", "bar": 1 },
"other": { "foo": 1 }
}
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)
}
}
{
"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.