TrailerJson

1.1.1

A feather-weight JSON decoder in Swift
ptsochantaris/trailer-json

What's New

Version 1.1.1

2024-02-18T17:14:58Z
  • Minor documentation updates.
  • Added benchmark that compares worst-case parsing of TypedJson parser to JSONSerialization, TrailerJson, and TypedJson best case.

Logo

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

  • Trailer
  • Trailer-CLI
  • Heavily tested and used in production with GitHub JSON v3 and v4 API payloads.

Detailed docs can be found here

The parsers

There are two parsers in this package:

  • TrailerJson will parse the entire data blob in one go, producing a dictionary much like JSONSerialization does.
  • TypedJson will quickly scan the data blob and provide results of type Entry, which have typed access (asInt, asFloat, asBool, asString, etc) and parses that data only when accessed.

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: Any]
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? [Any]
}
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

  • Supports UTF8 JSON data only
  • Uses native Swift data types in the results, no bridging overheads
  • null objects, fields, or array entries are thrown away, they are not kept
  • Floating point numbers are parsed as Float (i.e. not Double)
  • Does not support exponent numbers, only integers and floats
  • Does little to error-correct if the JSON feed isn't to spec

License

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

Description

  • Swift Tools 5.8.0
View More Packages from this Author

Dependencies

  • None
Last updated: Fri Apr 26 2024 17:21:16 GMT-0900 (Hawaii-Aleutian Daylight Time)