Home

Awesome

<img src="https://ptsochantaris.github.io/trailer/TrailerJsonLogo.webp" alt="Logo" width=256 align="right">

TrailerJson

A feather-weight JSON decoder in Swift with no dependencies. Is is roughly based on a version of Swift.org's open source replacement for the Apple JSONSerialisation framework.

Currently used in

Detailed docs can be found here

The parsers

There are two parsers in this package:

Compared to JSONSerialisation (when running optimised)

The TrailerJson parser performs almost equivalently BUT! the results are all native Swift types, so using those results incurs no bridging or copying costs, which is a major performance bonus.

The TypedJson parser is much faster, and ideal if you are only accessing a subset of the JSON data. It also makes it possible to parallelise the subsequent parsing in threads if needed.

Compared to Swift.org's version

Because it heavily trades features for decode-only performance, and that it returns native Swift types without the need to bridge them to ObjC for compatibility, it is by definition faster than the Swift.org version.

TL;DR

👍 Ideal for parsing stable and known service API responses like GraphQL, or on embedded devices. Self contained with no setup overhead.

👎 Bad at parsing/verifying potentially broken JSON, APIs which may suddenly include unexpected schema entries, or when you're better served by Decodable types.

Examples

let url = URL(string: "http://date.jsontest.com")!
let data = try await URLSession.shared.data(from: url).0
// TrailerJson - parse in one go to [String: Sendable]
if let json = try data.asJsonObject(),      // parse as dictionary
   let timeField = json["time"],
   let timeString = timeField as? String {
   
    print("The time is", timeString)
}
// TypedJson - scan the data and only parse 'time' as a String
if let json = try data.asTypedJson(),         // scan data
   let timeField = try? json["time"],
   let timeString = try? timeField.asString { // parse field
   
    print("The time is", timeString)
}

TrailerJson works directly with raw bytes so it can accept data from any type that exposes a raw byte buffer, such as NIO's ByteBuffer, without expensive casting or copies in-between:

let byteBuffer: ByteBuffer = ...
// TrailerJson
let jsonArray = try byteBuffer.withVeryUnsafeBytes { 
    try TrailerJson.parse(bytes: $0) as? [Sendable]
}
let number = jsonArray[1] as? Int
print(number)
// TypedJson
let jsonArray = try byteBuffer.withVeryUnsafeBytes { 
    try TypedJson.parse(bytes: $0)
}
let number = try jsonArray[1].asInt
print(number)
// TypedJson - using bytesNoCopy, lazy parsing (max performance, but with caveats!)
let number = try byteBuffer.withVeryUnsafeBytes { 

    // jsonArray and any Entry from it must not be accessed outside the closure 
    let jsonArray = try TypedJson.parse(bytesNoCopy: $0)

    // `secondEntry` reads from the original bytes, so it can't escape 
    let secondEntry = try jsonArray[1]

    // but parsed values can escape
    return try secondEntry.asInt
}
print(number)        

If you need to pass a TypedJson entry into a method that needs an untyped dictionary, you can eagerly parse a chunk by using the parse method - but beware that this can be slow for large sets of data, so it is best used for very specific cases!

// TypedJson - eager parsing (slowest performance)
let numberArray = try byteBuffer.withVeryUnsafeBytes { 

    // numbers and any Entry from it must not be accessed outside the closure 
    let numbers = try TypedJson.parse(bytes: $0)

    // but parsed value can escape - note that parsing the whole document would be 
    // very slow, so for cases like these the `TrailerJson` parser is 10x faster!
    return try numbers.parsed as! [Int]
}
let number = numberArray[1]
print(number)        

Notes

License

Copyright (c) 2023-2024 Paul Tsochantaris. Licensed under Apache License v2.0 with Runtime Library Exception, as per the open source material it is based on