I’m looking for a way to create an escape hatch in Encodable. There are many JSON APIs that are more flexible than Encodable allows for. As an example, take the Anthropic API which allows you to specify a JSON schema as part of the request body. This schema is not known ahead of time to the programmer, as the end user can craft it. See the input_schema
field here: https://docs.anthropic.com/en/docs/build-with-claude/tool-use
My goal is to enforce a strict contract where it makes sense (fixed fields) and allow
for an escape hatch of [String: Any]
when the structure if flexible. These aims are hard to achieve. I have been working on it for two days, writing various encoder hacks trying to get the desired output. What I have found is that it’s trivial to stay fully in the untyped world or fully in the strict contract world:
This is very flexible, and works out of the box:
let myTree: [String: Any] = [
"a": "xyz",
"b": [
"c": 2,
"d": false
]
]
do {
let data = try JSONSerialization.data(withJSONObject: myTree)
print(String(decoding: data, as: UTF8.self))
} catch {
print("Could not convert flexible dictionary to JSON: (error)")
}
This is very rigid, and works out of the box:
struct Root: Encodable {
let a: String
let b: Child
}
struct Child: Encodable {
let c: Int
let d: Bool
}
let myTree = Root(a: "xyz", b: Child(c: 2, d: false))
do {
let encoder = JSONEncoder()
encoder.outputFormatting = .sortedKeys
let data = try encoder.encode(myTree)
print(String(decoding: data, as: UTF8.self))
} catch {
print("Could not convert encodable struct to JSON: (error)")
}
If you run both of those, you’ll see that the two print statements produce the same JSON, great! Now assume that the structure of field b
isn’t known ahead of time, e.g. in the case of an API where the user specifies the schema. I want to do this:
struct RootWithEscapeHatch: Encodable {
let a: String
let b: [String: Any]
private enum Fields: CodingKey {
case a
case b
}
func encode(to encoder: any Encoder) throws {
var container = encoder.container(keyedBy: Fields.self)
try container.encode(self.a, forKey: .a)
// try container.encode(self.b, forKey: .b) <-- What goes here?
}
}
let myFailingTree = RootWithEscapeHatch(a: "xyz", b: ["c": 2, "d": false])
do {
let data = try JSONEncoder().encode(myFailingTree)
print(String(decoding: data, as: UTF8.self))
} catch {
print("Could not convert encodable with escape hatch to JSON: (error)")
}
You can see that myFailingTree
is isomorphic with myTree
examples above. I want the print statements to produce the same JSON. If you have an idea for what goes in the "What goes here?" line, please let me know. I’m looking for a general solution, I.e. I don’t want to hard code that the structure will always be ["c": 2, "d": false]
. The point is that any [String: Any]
field should be serializable just like the first example in this question.
Thanks!
3
Answers
I'm going to leave what I have here. It's not complete, and is still relatively easy to break. Making this approach correct is too cumbersome and hard to follow. @xAlien95's approach looks nice, and @mojtaba-hosseini's conclusion is worth heeding.
The approach below works for the tree in the original challenge, but you can still break it with this tree:
let myFailingTree = RootWithEscapeHatch(a: "xyz", b: ["c": [1, ["d": [2,false]]]])
Here are the utility functions:
Usage:
Prints:
{"a":"xyz","b":{"c":2,"d":false}}
🎮 Experimental solution:
Use
JSONSerialization
to encode the dictionary to a json string.This will cause the
b
value to be escaped again! So remove the escaping from the result (not like this):Result:
💡 Path to the redemption:
There are only 2 situations:
So maybe you needed something like the following pseudo code:
I hope this gives you some idea
You can use a custom
JSONValue
type instead of[String: Any]
:make it
Encodable
:and, optionally, easier to type by conforming it to the various
ExpressibleBy…Literal
protocols:With
JSONValue
you can then get what you’re looking for as follows:It is more generale than providing
[String: Any]
since you now can place anything asJSONValue
, e.g.nil
, scalar types or eterogeneous arrays.