KMP-Generated Kotlin — Idiosyncrasies for JS/TS Consumers

When the Solibo SDK is compiled to JavaScript via Kotlin Multiplatform, certain Kotlin types don’t map 1:1 to JS primitives. The SoliboClient facade handles the common conversions; this document also covers the remaining raw interop cases.


Quick reference

Kotlin type Facade/query result Raw interop
List<T> / KtList<T> JS arrays KtList.fromJsArray(arr)
List<Long> / KtList<bigint> JS arrays; ID inputs accept number \| string \| bigint KtList.fromJsArray(ids.map(BigInt))
Map<K,V> / KtMap JS Map KtMap.fromJsMap(map)
Long (API parameter) number \| string \| bigint BigInt(value)
Long (JSON serialization) Handled — BigInt polyfill converts to string n/a
Instant ISO-8601 string createInstant(date)
LocalDate ISO date string (YYYY-MM-DD) createLocalDate(year, month, day)
enum class .name (string key) or .value (string value) Pass SDK enum constant directly
Sealed / polymorphic Check .type string discriminator n/a

Prefer the facade

SoliboClient accepts ordinary JavaScript values and performs the KMP conversion internally:

import { createSoliboClient } from '@solibo/solibo-sdk'

const client = createSoliboClient({
  auth: { kind: 'browser', userPoolId: '...', clientId: '...' },
})
const tasks = await client.api.task.indexTasks({ companyId })

Use client.raw or @solibo/solibo-sdk/interop only when you need response envelopes, request headers, redirects, downloads, or direct Kotlin collection construction.


1. Paged Lists — PagedList<T>

All collection endpoints return a PagedList<T> instead of a plain list. The wrapper has three fields:

Field Type Description
.items KtList<T> The current page of results
.meta Meta Metadata — .count is the number of items on this page
.paging Paging Cursor tokens — .next / .previous for navigation; .next === "END_OF_LIST" means no more pages

In hooks (solibo-react)

Paged list hooks use useInfiniteQuery. Data is accumulated across pages in data.pages:

const { data, fetchNextPage, hasNextPage } = useIssues({ companyId })

// Flatten all pages into one array
const allIssues = data?.pages.flatMap(page => page.items) ?? []

Each page in data.pages is already a plain JS object — page.items is a readonly T[] array because SoliboClient normalizes KMP collections at the SDK boundary.

Directly from the SDK (outside a hook)

const body = await client.api.task.indexTasks({ companyId })
const tasks = body.items                          // T[]
const nextToken = body.paging.next                // string | null

2. Collections — KtList / KtMap

Reading (queries)

solibo-query receives normalized results from SoliboClient, so React components get plain arrays without importing or invoking bridge helpers.

If you work directly with the facade outside a hook, collection fields are already arrays:

const response = await client.api.task.indexTasks({ companyId })
const items = response.items
// items is a readonly T[] — iterate, map, find, etc.

Manual KtList conversion is only needed after an explicit client.raw call.

Writing (commands)

Facade command inputs accept plain arrays:

client.api.sections.multiCreateTagOnSection({ companyId, sectionId, input: tags })

Raw callers construct KMP collections explicitly:

import { KtList } from '@solibo/solibo-sdk/interop'

client.raw.api.thirdParty.sendWithDigiPost(
    BigInt(companyId),
    BigInt(thirdPartyId),
    KtList.fromJsArray(publishTo.map(BigInt))
)

For maps:

import { KtMap } from '@solibo/solibo-sdk/interop'

const ktMap = KtMap.fromJsMap(new Map(Object.entries(myObj)))

Use KtMutableList.fromJsArray() / KtMutableMap.fromJsMap() when the API requires a mutable variant.


3. Long — API parameters

Kotlin Long becomes a bigint on the raw JS side. Facade IDs accept number, string, or bigint and convert internally:

client.api.organization.showOrganization({ organizationId })
client.api.task.indexTasks({ companyId })

// Raw positional interop still requires bigint:
client.raw.api.task.indexTasks(BigInt(companyId))

IDs returned in responses are also bigint. When comparing or using them as React Query keys, convert to string or number as needed:

const id = response.id.toString()      // safe for display, keys, URL params
const idNum = Number(response.id)      // only safe for values < 2^53

Serialization (handled automatically)

solibo-query ships a bigint-polyfill.ts that patches BigInt.prototype.toJSON to serialize as a string. This is imported at package entry and requires no action from consumers.


4. Date and time — Instant and LocalDate

Facade inputs and results use ISO strings for Kotlin date/time values:

// For LocalDate fields (e.g. startDate, endDate):
'2025-01-31'

// For Instant fields (e.g. validFrom, closedAt):
new Date().toISOString()

When reading date fields from responses, convert to a JS Date for display:

const date = new Date(response.createdAt)

Do not pass a raw Date object to the facade. Use toISOString() for Instant fields and a YYYY-MM-DD string for LocalDate fields. Raw KMP callers can construct the corresponding values with createInstant(date) and createLocalDate(year, month, day) from @solibo/solibo-sdk/interop.


5. Enums

Kotlin enums compile to singleton objects, not plain strings. Two properties are available:

  • .name — the enum entry name as declared in Kotlin ("APPROVED", "FOR_APPROVAL")
  • .value — the JSON serialization value (usually the same string)
// Reading:
expense.status.name === 'APPROVED'    // true
expense.status.value === 'APPROVED'   // true (usually identical)

// Comparing with SDK constant (preferred — survives renames):
expense.status === ExpenseStatus.APPROVED

// Facade inputs accept either the enum constant or its documented string value:
client.api.expense.createExpense({
  companyId,
  input: { status: ExpenseStatus.FOR_APPROVAL, /* ... */ },
})

Do not compare a returned enum object directly with a raw string using ===; use .name, .value, or the SDK constant.


6. Sealed classes / polymorphic types

The API returns polymorphic types (e.g. Resident is a sealed superclass; PersonResident and OrgResident extend it). The concrete type is identified by a type field whose value is the fully-qualified Kotlin class name:

if (resident.type === 'solibo.residents.query.domain.PersonResident') {
    // narrow to PersonResident — access .email, .name, .mobile, etc.
}

instanceof checks do not work across the Kotlin/JS boundary.


7. null vs undefined

Kotlin nullable fields (String?, Long?, etc.) arrive as null in JS, not undefined. Use null-coalescing or loose null checks:

resident.email ?? 'fallback'     // correct
resident.email !== undefined     // may miss null — incorrect
resident.email != null           // correct (covers both)

Solibo AS