Skip to content

Add Kotlin serialization-based APIs#475

Open
snej wants to merge 3 commits into
masterfrom
feature/kotlin-serialization
Open

Add Kotlin serialization-based APIs#475
snej wants to merge 3 commits into
masterfrom
feature/kotlin-serialization

Conversation

@snej
Copy link
Copy Markdown
Contributor

@snej snej commented May 8, 2026

Implements Kotlin extension methods that use serialization to allow developers to work with documents and query rows as native Kotlin classes. (Equivalent to the Swift API extensions implemented last year.)

Query Results & ResultSets

/** Uses Kotlin Serialization to create an object of type [T] from the query row.
 *  If the [key] parameter is non-null, it uses the row's value for that key as the source,
 *  instead of the entire row.
 *
 *  For example, if your query is `SELECT name, age, shoeSize FROM people` and you have a
 *  serializable Person class with properties name, age and shoeSize, you can call:
 *  `val person = result.data<Person>()`
 *
 *  Or you could use the query `SELECT * as person FROM people` and create the Person with
 *  `val person = result.data<Person>("person")`.
 */
@ExperimentalSerializationApi
inline fun <reified T> Result.data(key: String? = null): T

/** Returns the query rows transformed by Kotlin Serialization into instances of [T]. */
@ExperimentalSerializationApi
inline fun <reified T> ResultSet.data(key: String? = null): Sequence<T>

Documents

Document model classes must implement a new interface DocumentModel that defines a documentMeta property that stores the document id and revID, which is needed in order to save the document.

/** Document model classes must implement this interface.
 *  It adds a [documentMeta] property that's used by Couchbase Lite. */
interface DocumentModel {
    /** This tags the model instance with the document ID and revision it was read from,
     *  which enables conflict detection when it's later saved.
     *  You may read this property, but DO NOT alter it.
     *  It should be implemented as a stored property defaulting to `null`, for example:
     *  `@Transient override var documentMeta: DocumentMeta? = null` */
    @Transient var documentMeta: DocumentMeta?
}

/** Stores the Couchbase Lite metadata of a document. Used by the [DocumentModel] interface. */
class DocumentMeta internal constructor(val collection: Collection?,
                                        val id: String,
                                        val revisionID: String)

Documents can be loaded and saved as model instances:

/** Gets an existing document with the given ID, and uses Kotlin Serialization to create an
 *  instance of class [T] from it. [T] must implement [DocumentModel].
 *  If a document with the given ID doesn't exist in the collection, returns null. */
@ExperimentalSerializationApi
inline fun <reified T: DocumentModel> Collection.getDocumentAs(id: String): T? =
    getDocumentAs(id, serializer())

/** Saves a [DocumentModel] instance as a document in the collection, with a specified conflict handler.
 *  If the model's [DocumentModel.documentMeta] property is null, it will be saved as a new document with the
 *  given [docID], which must not be null.
 *  Otherwise the [DocumentModel.documentMeta] property determines the document ID and prior revision ID, and the
 *  [docID] parameter should be null.
 *  After a successful save, the [DocumentModel.documentMeta] property is updated to the current state. */
@ExperimentalSerializationApi
inline fun <reified T: DocumentModel> Collection.save(model: T,
                                                      docID: String? = null,
                                                      noinline conflictHandler: ModelConflictHandler<T>? = null) =

Lower-Level

Both of these use some new internal functions that serialize Kotlin classes to and from Fleece.

/** Uses Kotlin Serialization to encode an arbitrary object or collection to Fleece.
 *  @param value  The object to encode.
 *  @param encoder  A Fleece [FLEncoder] to use; defaults to a fresh instance.
 *  @return  The encoded Fleece data. */
@ExperimentalSerializationApi
inline fun <reified T> serializeToFleece(value: T, encoder: FLEncoder? = null): ByteArray

/** Decodes an object from a Fleece [FLValue] using Kotlin Serialization. */
@ExperimentalSerializationApi
inline fun <reified T> deserializeFromFleece(root: FLValue): T

snej added 2 commits May 7, 2026 12:16
- Collection.getDocumentAs(), save()
- Result.data()
- ResultSet.data()
@snej
Copy link
Copy Markdown
Contributor Author

snej commented May 8, 2026

I just noticed that Swift has model-based Collection.delete and Collection.purge methods; I'll add those.

@snej snej requested a review from pasin May 11, 2026 19:26

/** Saves a [DocumentModel] instance as a document in the collection, with a specified conflict handler.
* If the model's [DocumentModel.documentMeta] property is null, it will be saved as a new document with the
* given [docID], which must not be null.
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In Swift, we also support id == nil as an auto generated id for a newly created doc. Can we allow that here as well?

val doc: MutableDocument
if (meta == null) {
require(docID != null) { "docID argument must be given when saving a new document" }
doc = MutableDocument(docID)
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we also allow null docID here (an auto id (uuid) will be generated)?

private fun <T:DocumentModel> MutableDocument.setContentFromModel(model: T, serializer: SerializationStrategy<T>) {
val body = serializeToFleece(serializer, model)
val root = FLValue.fromData(body).asFLDict()
setContent(root, false)
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does the body need to be retained as well?

I read iOS code, and iOS is doing something similar. If I read the code correctly, Swift's DocumentEncoder calls FLEncoder_FinishDoc that returns an FLDoc, get the dict from the FLDoc, set the dict to the CBL doc, and abandon the FLDoc. So maybe the data is retained by the dict?

- (BOOL)finishIntoDocument:(CBLDocument*)document error:(NSError**)outError {
    FLError error {};
    FLDoc fldoc = FLEncoder_FinishDoc(_encoder, &error);
    if (!fldoc) {
        return convertError(error, outError);
    }
    Doc doc { fldoc };
    Dict fleeceData = doc.asDict();
    if (!fleeceData) {
        return NO;
    }
    [document setFleece: (FLDict)fleeceData];
    return YES;
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants