JSON Interoperability Guide
It is often useful to convert data that was retrieved from MongoDB to JSON, either for producing a human readable
version of it or for serving it up via a REST API. BSON (the format that MongoDB uses to store data)
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,
which is a standard format of JSON used by the various drivers to represent BSON data in JSON that includes extra
information indicating the BSON type of a given value. If preserving the type information isn’t required,
then Foundation’s JSONEncoder
and JSONDecoder
can be used to convert the data to regular JSON, though not all
BSON types currently support working with them (e.g. BSONBinary
).
Extended JSON
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.
The two formats of extended JSON are as follows:
- Relaxed Extended JSON - A string format based on the JSON standard that describes BSON documents.
Relaxed Extended JSON emphasizes readability and interoperability at the expense of type preservation.
- example:
{"d": 5.5}
- example:
- Canonical Extended JSON - A string format based on the JSON standard that describes BSON documents.
Canonical Extended JSON emphasizes type preservation at the expense of readability and interoperability.
- example:
{"d": {"$numberDouble": 5.5}}
- example:
Here we can see the same data: a key, "i"
with the value 1
represented in BSON, and two forms of Extended JSON
// BSON
"0C0000001069000100000000"
// Relaxed Extended JSON
{"i": 1}
// Canonical Extended JSON
{"i": {"$numberInt":"1"}}
To see how all of the BSON types are represented in Canonical and Relaxed Extended JSON Format, see the documentation here.
A thorough example Canonical Extended JSON document and its relaxed counterpart can be found here.
Generating and Parsing Extended JSON via Codable
The ExtendedJSONEncoder
and ExtendedJSONDecoder
provide a way for any custom Codable
classes to interact with
canonical or relaxed extended JSON. They can be used just like JSONEncoder
and JSONDecoder
.
let encoder = ExtendedJSONEncoder()
let decoder = ExtendedJSONDecoder()
struct Person: Codable, Equatable {
let name: String
let age: Int32
}
let bobExtJSON = try encoder.encode(Person(name: "Bob", age: 25)) // "{\"name\":\"Bob\",\"age\":25}}"
let joe = try decoder.decode(Person.self, from: "{\"name\":\"Joe\",\"age\":34}}".data(using: .utf8)!)
The ExtendedJSONEncoder
produces relaxed Extended JSON by default, but can be configured to produce canonical.
let bob = Person(name: "Bob", age: 25)
let encoder = ExtendedJSONEncoder()
encoder.mode = .canonical
let canonicalEncoded = try encoder.encode(bob) // "{\"name\":\"Bob\",\"age\":{\"$numberInt\":\"25\"}}"
The ExtendedJSONDecoder
accepts either format, or a mix of both:
let decoder = ExtendedJSONDecoder()
let canonicalExtJSON = "{\"name\":\"Bob\",\"age\":{\"$numberInt\":\"25\"}}"
let canonicalDecoded = try decoder.decode(Person.self, from: canonicalExtJSON.data(using: .utf8)!) // bob
let relaxedExtJSON = "{\"name\":\"Bob\",\"age\":25}}"
let relaxedDecoded = try decoder.decode(Person.self, from: relaxedExtJSON.data(using: .utf8)!) // bob
Using Extended JSON with Vapor
By default, Vapor uses JSONEncoder
and JSONDecoder
for encoding and decoding its Content
to and from JSON.
If you are interested in using the ExtendedJSONEncoder
and ExtendedJSONDecoder
in your
Vapor app instead, you can set them as the default encoder and decoder and thereby allow your
application to serialize and deserialize data to/from Extended JSON, rather than the default plain JSON.
This is recommended because not all BSON types currently support working with JSONEncoder
and JSONDecoder
and
also so that you can take advantage of the added type information.
From the Vapor Documentation:
you can set the global configuration and change the encoders and decoders Vapor uses by default
by doing something like this:
let encoder = ExtendedJSONEncoder()
let decoder = ExtendedJSONDecoder()
ContentConfiguration.global.use(encoder: encoder, for: .json)
ContentConfiguration.global.use(decoder: decoder, for: .json)
in your configure.swift
.
In order for this to work, you will also have to include extensions that ensure conformance to Vapor’s
ContentEncoder
and ContentDecoder
protocols. The snippets below should be sufficient for doing that.
extension ExtendedJSONEncoder: ContentEncoder {
public func encode<E>(_ encodable: E, to body: inout ByteBuffer, headers: inout HTTPHeaders) throws
where E: Encodable
{
headers.contentType = .json
try body.writeBytes(self.encode(encodable))
}
}
extension ExtendedJSONDecoder: ContentDecoder {
public func decode<D>(_ decodable: D.Type, from body: ByteBuffer, headers: HTTPHeaders) throws -> D
where D: Decodable
{
let data = body.getData(at: body.readerIndex, length: body.readableBytes) ?? Data()
return try self.decode(D.self, from: data)
}
}
To see some example Vapor apps using the driver, check out Examples/VaporExample or Examples/ComplexVaporExample.
Using JSONEncoder
and JSONDecoder
with BSON Types
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.
let foo = Foo(x: BSONObjectID(), date: Date(), y: 3.5)
try JSONEncoder().encode(foo) // "{\"x\":<hexstring>,\"date\":<seconds since reference date>,\"y\":3.5}"