|
| 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