Skip to content

Commit e4a7e9a

Browse files
committed
Create JSON-Interop.md
1 parent dd0ffda commit e4a7e9a

File tree

1 file changed

+128
-0
lines changed

1 file changed

+128
-0
lines changed

Guides/JSON-Interop.md

Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
# JSON Interoperability Guide
2+
It is often useful to convert data that was retrieved from MongoDB to JSON, either for producing a human readable
3+
version of it or for serving it up via a REST API. [BSON](bsonspec.org) (the format that MongoDB uses to store data)
4+
supports more types than JSON does though, which means JSON alone can't represent BSON data losslessly. To solve this issue, you can convert your data to [Extended JSON](https://docs.mongodb.com/manual/reference/mongodb-extended-json/),
5+
which is a standard format of JSON used by the various drivers to represent BSON data in JSON that includes extra
6+
information indicating the BSON type of a given value. If preserving the type information isn't required,
7+
then Foundation's `JSONEncoder` and `JSONDecoder` can be used to convert the data to regular JSON, though not all
8+
BSON types currently support working with them (e.g. `BSONBinary`).
9+
10+
## Extended JSON
11+
12+
As mentioned above, Extended JSON is a form of JSON that preserves type information. There are two forms of extended JSON, and the form used determines how much extra type information is included in the JSON format for a given type.
13+
14+
The two formats of extended JSON are as follows:
15+
- _Relaxed Extended JSON_ - A string format based on the JSON standard that describes BSON documents.
16+
Relaxed Extended JSON emphasizes readability and interoperability at the expense of type preservation.
17+
- example: `{"d": 5.5}`
18+
- _Canonical Extended JSON_ - A string format based on the JSON standard that describes BSON documents.
19+
Canonical Extended JSON emphasizes type preservation at the expense of readability and interoperability.
20+
- example: `{"d": {"$numberDouble": 5.5}}`
21+
22+
23+
Here we can see the same data: a key, `"i"` with the value `1` represented in BSON, and two forms of Extended JSON
24+
```
25+
// BSON
26+
"0C0000001069000100000000"
27+
28+
// Relaxed Extended JSON
29+
{"i": 1}
30+
31+
// Canonical Extended JSON
32+
{"i": {"$numberInt":"1"}}
33+
```
34+
To see how all of the BSON types are represented in Canonical and Relaxed Extended JSON Format, see the documentation
35+
[here](https://docs.mongodb.com/manual/reference/mongodb-extended-json/#bson-data-types-and-associated-representations).
36+
37+
A thorough example Canonical Extended JSON document and its relaxed counterpart can be found
38+
[here](https://github.com/mongodb/specifications/blob/master/source/extended-json.rst#canonical-extended-json-example).
39+
40+
### Generating and Parsing Extended JSON via `Codable`
41+
The `ExtendedJSONEncoder` and `ExtendedJSONDecoder` provide a way for any custom `Codable` classes to interact with
42+
canonical or relaxed extended JSON. They can be used just like `JSONEncoder` and `JSONDecoder`.
43+
```swift
44+
let encoder = ExtendedJSONEncoder()
45+
let decoder = ExtendedJSONDecoder()
46+
47+
struct Person: Codable, Equatable {
48+
let name: String
49+
let age: Int32
50+
}
51+
52+
let bobExtJSON = try encoder.encode(Person(name: "Bob", age: 25)) // "{\"name\":\"Bob\",\"age\":25}}"
53+
let joe = try decoder.decode(Person.self, from: "{\"name\":\"Joe\",\"age\":34}}".data(using: .utf8)!)
54+
```
55+
56+
The `ExtendedJSONEncoder` produces relaxed Extended JSON by default, but can be configured to produce canonical.
57+
```swift
58+
let bob = Person(name: "Bob", age: 25)
59+
let encoder = ExtendedJSONEncoder()
60+
encoder.mode = .canonical
61+
let canonicalEncoded = try encoder.encode(bob) // "{\"name\":\"Bob\",\"age\":{\"$numberInt\":\"25\"}}"
62+
```
63+
The `ExtendedJSONDecoder` accepts either format, or a mix of both:
64+
```swift
65+
let decoder = ExtendedJSONDecoder()
66+
67+
let canonicalExtJSON = "{\"name\":\"Bob\",\"age\":{\"$numberInt\":\"25\"}}"
68+
let canonicalDecoded = try decoder.decode(Person.self, from: canonicalExtJSON.data(using: .utf8)!) // bob
69+
70+
let relaxedExtJSON = "{\"name\":\"Bob\",\"age\":25}}"
71+
let relaxedDecoded = try decoder.decode(Person.self, from: relaxedExtJSON.data(using: .utf8)!) // bob
72+
```
73+
74+
### Using Extended JSON with Vapor
75+
By default, [Vapor](https://docs.vapor.codes/4.0/) uses `JSONEncoder` and `JSONDecoder` for encoding and decoding its [`Content`](https://docs.vapor.codes/4.0/content/) to and from JSON.
76+
If you are interested in using the `ExtendedJSONEncoder` and `ExtendedJSONDecoder` in your
77+
Vapor app instead, you can set them as the default encoder and decoder and thereby allow your
78+
application to serialize and deserialize data to/from Extended JSON, rather than the default plain JSON.
79+
This is recommended because not all BSON types currently support working with `JSONEncoder` and `JSONDecoder` and
80+
also so that you can take advantage of the added type information.
81+
From the [Vapor Documentation](https://docs.vapor.codes/4.0/content/#override-defaults):
82+
you can set the global configuration and change the encoders and decoders Vapor uses by default
83+
by doing something like this:
84+
85+
```swift
86+
let encoder = ExtendedJSONEncoder()
87+
let decoder = ExtendedJSONDecoder()
88+
ContentConfiguration.global.use(encoder: encoder, for: .json)
89+
ContentConfiguration.global.use(decoder: decoder, for: .json)
90+
```
91+
in your `configure.swift`.
92+
93+
In order for this to work, you will also have to include extensions that ensure conformance to Vapor's
94+
`ContentEncoder` and `ContentDecoder` protocols. The snippets below should be sufficient for doing that.
95+
```swift
96+
extension ExtendedJSONEncoder: ContentEncoder {
97+
public func encode<E>(_ encodable: E, to body: inout ByteBuffer, headers: inout HTTPHeaders) throws
98+
where E: Encodable
99+
{
100+
headers.contentType = .json
101+
try body.writeBytes(self.encode(encodable))
102+
}
103+
}
104+
```
105+
106+
```swift
107+
extension ExtendedJSONDecoder: ContentDecoder {
108+
public func decode<D>(_ decodable: D.Type, from body: ByteBuffer, headers: HTTPHeaders) throws -> D
109+
where D: Decodable
110+
{
111+
let data = body.getData(at: body.readerIndex, length: body.readableBytes) ?? Data()
112+
return try self.decode(D.self, from: data)
113+
}
114+
}
115+
```
116+
117+
To see some example Vapor apps using the driver, check out
118+
[Examples/VaporExample](https://github.com/mongodb/mongo-swift-driver/tree/master/Examples/VaporExample) or
119+
[Examples/ComplexVaporExample](https://github.com/mongodb/mongo-swift-driver/tree/master/Examples/ComplexVaporExample).
120+
121+
## Using `JSONEncoder` and `JSONDecoder` with BSON Types
122+
123+
Currently, some BSON types (e.g. `BSONBinary`) do not support working with encoders and decoders other than those introduced in `swift-bson`, meaning Foundation's `JSONEncoder` and `JSONDecoder` will throw errors when encoding or decoding such types. There are plans to add general `Codable` support for all BSON types in the future, though. For now, only `BSONObjectID` and any BSON types defined in Foundation or the standard library (e.g. `Date` or `Int32`) will work with other encoder/decoder pairs. If type information is not required in the output JSON and only types that include a general `Codable` conformance are included in your data, you can use `JSONEncoder` and `JSONDecoder` to produce and ingest JSON data.
124+
125+
``` swift
126+
let foo = Foo(x: BSONObjectID(), date: Date(), y: 3.5)
127+
try JSONEncoder().encode(foo) // "{\"x\":<hexstring>,\"date\":<seconds since reference date>,\"y\":3.5}"
128+
```

0 commit comments

Comments
 (0)