As part of trying to optimise time spent on large queries (fetch + decode), i am trying to find the fastest solution to decode results.
At the moment, the fastest way i found by my benchmarking that works reliably is manually mapping the values inside a result to my local object. While this adds a good chunk of boilerplate, going through .toJSON(), then serialising the JSON and then decoding it is much slower in my tests.
However, there is another API in the CouchbaseLiteSwift package both on Result and ResultsSet (.data(as: _)) that I would like to test but that seems to break for dates.
So my questions are:
Is the .data(as: _) API broken or am I doing something wrong?
What is the fastest supported way to decode a result, regardless of how much code it implies?
Thank you!
Example for reference:
struct Foo: Codable {
let id: String
let name: String
let someDate: Date?
// ALWAYS FAILS
static func fromData(_ result: Result) throws -> Foo {
try result.data(as: Foo.self)
}
// WORKS RELIABLY (fastest decode strategy i found thus far)
static func fromFields(result: Result) throws -> Foo {
guard
let id = result.string(forKey: "id"),
let name = result.string(forKey: "name")
else {
throw NSError()
}
let someDate = result.date(forKey: "someDate")
return Foo(
id: id,
name: name,
someDate: someDate
)
}
}
Breaks as in the decode fails for date properties. This is the thrown error, tested with the date no longer optional:
**Error Domain=CouchbaseLite Code=180
01 "Failed to parse ISO8601 Date from '2025-05-30T13:13:41.883Z'" UserInfo={NSLocalizedDescription=Failed to parse ISO8601 Date from '2025-05-30T13:13:41.883Z'}**
It looks like the date formatter that the .date(as:_) API uses does not match the formatter that MutableDocument.setDate(_, forKey:_)uses.
Thanks for pinging the team.
Here is some additional info that might reduce debugging time:
Because of the decode failure, i think under the hood the .data(as:_) API might be using either the default swift DateFormatter or ISO8601DateFormatter, the point would be valid for both.
While the date string is indeed in accordance to the ISO8601 standard, none of these formatters can successfully decode this specific version of the format.
The CouchbaseLiteSwift sdk stores dates in the following format: YYYY-MM-DDTHH:MM:SS.sssZ
The default Swift DateFormatter() cannot decode any ISO8601 dates out of the box without custom configuration, so using that would be a no go.
The default swift ISO8601DateFomatter() contrary to what you would believe can not decode all iso8601 formats. It can only specifically decode the following format: YYYY-MM-DDTHH:MM:SSZ.
This means that neither date formatters can decode the format couchbase stores out of the box.
However as I am sure you know, both formatters can be customised. I would recommend using the ISO8601DateFormatter as it is faster at conversions than the basic one. Either way, both these formatters would work:
I don’t mean to be rude or anything… I do not know where this idiom came from. It is just a completely horrible idea. It is hard to imagine how anything could be slower… and it is going to guarantee all kinds of type conversion errors.
The right thing to do, in Java, is to have a wrapper class for your data objects:
new DbData(myDataObject).toMutableDocument()
new DBData(mutableDoc).toMyData()
This is even easier in Swift/Kotlin, where there are extension methods:
(FWIW, I suppose you could add a “save()” method for your data type, on Database and avoid touching the MutableDocument all together. Whatever.)
Take a few minutes to write the boilerplate that stores the props of your data object, directly into the mutable document. Don’t waste the hours of CPU time that that gee-whiz round trip through a JSON string consumes.
Given that i mentioned in my post that manually mapping values even if boilerplate-y is the fastest way i already found and that json serialisation is provably slower, your comment is truly helpful and provides much needed insight indeed.
As to where this wild idea of trying to go through json came from and how anyone using couchbase for the first time might ever come up with something as stupid as trying to do this, may i point you to the official docs as one place that might, perhaps, lead an unassuming developer down such a obviously dumb path? Documents | Couchbase Docs
When it comes to it being hard to imagine anything being slower, i can tell you decisively that at least in with swift sdk, when decoding a large amount of documents, json + decoding is significantly faster than using the CouchbaseLiteSwift sdk’s own decoding api result.data(as: _).
I hope this clarifies why, having already found and tested 4 ways to convert query results officially supported by the sdk - direct value mapping using result value getters (ex. result.string(forKey: _)), using json + decoding (result.toJSON()), using swift dictionary conversion (result.toDictionary()) and using the sdk provided decoding api (result.data(as: _)) - with varying degrees of success and surprising results, i would be curious if there is yet another way of doing things even faster that i have simply not yet discovered.
(And btw, since my first post i have found that on large data sets, the horrible idea of using .toJSON() + decoding is actually faster than manual value mapping if the data model being decoded into does not contain dates. Just thought you might find that interesting)
@tudor.andreescu That would be my fault on the Date formatter. Thanks for the diagnosis. Will file an issue to add this case to our tests and fix the formatter.
The point on performance is definitely interesting. I wonder if .toJSON() being faster than the value getters when Dates are involved is also causing result.data(as:) to be slower for a large number of documents. AKA maybe our Date parsing needs a fix for performance. I will file an issue to investigate the performance issues, but that won’t be as high priority as fixing the .data(as:) date parsing.
Thanks for the thorough analysis
I will attach a self contained test suite file adapted from my project that you can drop into any xcode project that imports the couchbase swift sdk. This test suite creates a temporary database, writes a large (configurable) amount of entries and then does performance tests set up for all decoding paths i could think of. Finally it will delete the test db.
You will see that wether manual mapping is faster than json decoding depends entirely on these two lines inside toCouchbaseDocumentUsingKeyValueSetting being commented out or not:
if let sampleDate = sampleDate { doc.setDate(sampleDate, forKey: "sampleDate") }
if let sampleDate2 = sampleDate2 { doc.setDate(sampleDate2, forKey: "sampleDate2") }
Also, i find it helpful to use "seconds)" as the filtering term inside the xcode console. You can then easily see how the individual tests performed when running all of them at the same time (to make sure they are doing the same work, on the same data set)
example of output with dates commented out (let sampleSet = MockModel.random(count: 10000)) :
Test Case '-[dummyTests.PerformanceTests test_decodingASPerformance]' passed (7.515 seconds).
Test Case '-[dummyTests.PerformanceTests test_decodingManualMapping]' passed (2.717 seconds).
Test Case '-[dummyTests.PerformanceTests test_decodingToDictPerformance_packedValues]' passed (4.247 seconds).
Test Case '-[dummyTests.PerformanceTests test_decodingToDictPerformance_unpackedValues]' passed (3.988 seconds).
Test Case '-[dummyTests.PerformanceTests test_decodingToJSONPerfomance_container]' passed (2.322 seconds).
Test Case '-[dummyTests.PerformanceTests test_decodingToJSONPerfomance_contentsOf]' passed (2.384 seconds).
Test Case '-[dummyTests.PerformanceTests test_decodingToJSONPerformance_valuesFirst]' passed (2.387 seconds).
example of output with dates being decoded (let sampleSet = MockModel.random(count: 10000)):
Test Case '-[dummyTests.PerformanceTests test_decodingASPerformance]' passed (7.621 seconds).
Test Case '-[dummyTests.PerformanceTests test_decodingManualMapping]' passed (2.962 seconds).
Test Case '-[dummyTests.PerformanceTests test_decodingToDictPerformance_packedValues]' passed (8.153 seconds).
Test Case '-[dummyTests.PerformanceTests test_decodingToDictPerformance_unpackedValues]' passed (7.877 seconds).
Test Case '-[dummyTests.PerformanceTests test_decodingToJSONPerfomance_container]' passed (6.011 seconds).
Test Case '-[dummyTests.PerformanceTests test_decodingToJSONPerfomance_contentsOf]' passed (6.103 seconds).
Test Case '-[dummyTests.PerformanceTests test_decodingToJSONPerformance_valuesFirst]' passed (6.101 seconds).
@tudor.andreescu Yes. I humbly apologize for those documents. I was not aware that we were actually promoting that idiom and I’ve lodged a complaint about them with the responsible parties.
Some of that sample code appears to have been written by someone who didn’t know either Android or Couchbase very well. Sigh.
I understand that the Swift team is aware of the inefficiencies in their marshalling/unmarshalling code and is working on it.
As for the last bit, I find the idea that using JSON as an intermediate step is faster not just interesting, but completely unbelievable. I’d be interested to hear exactly what you are testing.
You can have a look at exactly what i am testing inside the file i uploaded as part of my last comment (i am testing exclusively fetch + decoding speed, but the fetch is the same among all test cases so the difference maker is the actual decoding strategy).
If you have access to a mac and xcode, running the test suite i provided should be trivial. If you don’t, then understanding the code inside should be relatively trivial too. I have already sent some sample results of running that test suite above that you could reference, both with and without dates in the decoded document.
Will have a look. Thanks!
Since this is Swift, I may not have much to add.
Later:
FWIW, I believe that this encodedDocument.dictionary, is a copy (it is in Java). test_decodingManualMapping contains several of them. If you are looking for max speed, you might try avoiding that.
I don’t know the semantics of Swift, so I don’t know whether copy = encodedDocuments is.