Primitives and strings
Kotlin primitives and strings appear as ordinary C# values. Nullable types keep their ?.
For this declaration in Mappings.kt:
Call it from C#:
Type mappings
Kotlin | C# |
|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
These mappings describe Kotlin APIs exported to C#. For consuming a C# library from Kotlin, see Types that cross the wire; nullable value types and the DateTimeOffset, TimeSpan, and Guid mappings are not supported in that direction.
Nullable values
Int? becomes int?, Boolean? becomes bool?, and String? becomes string?. Instant?, Duration?, and Uuid? become DateTimeOffset?, TimeSpan?, and Guid?. null stays distinct from 0, false, an empty string, or Guid.Empty.
A property getter returning a nullable primitive, Instant?, or Duration? is evaluated twice when it returns a value. Keep those getters free of side effects and ensure their result stays stable between evaluations. A top-level function with the same return evaluates once, like an ordinary instance method.
Passing null for a non-null String
C# lets null! through where a string is declared. For a Kotlin String (not String?) the generated code refuses it before anything crosses, with an ArgumentNullException whose ParamName is the parameter you wrote:
This covers constructor, method, setter (ParamName is value), suspend, top-level, companion, object, extension receiver and sealed arm parameters alike. A null inside a List, Set or Map of strings is reported against the collection you passed, so ParamName is texts for desk.StampAll(new List<string> { "a", null! }); it replaces the KotlinNullReferenceException an earlier build threw there. On a member returning a Flow, the exception is thrown when the flow is collected, because that is where Kotlin reads the argument.
Two shapes accept null on purpose: a string? parameter (including string? x = null), and a generic T position (new Box<string>(null!) holds the null, since T is unbounded). A value class over a String has its own guard and throws ArgumentException for default.
A lambda or C#-implemented interface member that returns null where Kotlin expects a non-null String is refused the same way. See Exceptions from a callback.
Char
Char maps to C# char, including non-ASCII characters. It works in properties, parameters, returns, and collections.
Char? becomes C# char? everywhere a bare Char? appears: a property, a constructor or method parameter, a method return, and a top-level function return. null stays distinct from every character, including non-ASCII ones.
Use String for characters that need a surrogate pair; lone surrogate values do not reliably survive the conversion.
Instant
Use DateTimeOffset for Kotlin's kotlin.time.Instant:
log.FirstSeen represents the same instant as at, with its offset normalized to UTC. Use .UtcDateTime if you need a DateTime.
C# inputs preserve their full precision. Kotlin results lose fractions smaller than 100 ns, rounding down to the preceding tick.
Results must be within years 0001–9999. Out-of-range values, including
Instant.DISTANT_PASTandInstant.DISTANT_FUTURE, throwKotlinArgumentException.The older
kotlinx.datetime.Instanttype is not mapped.
Duration
Use TimeSpan for Kotlin's kotlin.time.Duration:
Kotlin results truncate toward zero to 100 ns precision.
All
TimeSpaninputs are accepted. Values beyond roughly ±146 years lose sub-millisecond precision in Kotlin, so a round trip may return a slightly different value.Infinite durations and finite values outside
TimeSpan's range (about ±10,675,199 days) throwKotlinArgumentException.
Uuid
Use Guid for Kotlin's kotlin.uuid.Uuid:
record.Id equals minted. All 128 bits are preserved, and Uuid.NIL maps to Guid.Empty.
Instant, Duration, and Uuid work as properties, constructor and method parameters, and function results, and as a suspend or Flow parameter, result or element (see Coroutines and Flow). They also work as List, Set and Map elements, keys and values, nullable included: each element crosses as ticks (Instant, Duration) or text (Uuid).
See Collections for the shared rules. A value class over one of these types binds too (see Value classes). For extension receiver support, see Extensions.
ByteArray
Use byte[] for Kotlin's ByteArray. A byte[] argument or result is always a fresh copy: nothing you do to it on either side reaches the other.
An empty ByteArray (byteArrayOf()) crosses as Array.Empty<byte>(), never null. ByteArray? becomes byte[]?, with null distinct from an empty array; payload.Checksum above is null for an empty payload. ByteArray works as a property, constructor and method parameter, and function result, and as a List element or a Map value (List<ByteArray> is IReadOnlyList<byte[]>, see Collections). It is not supported as a Set element or a Map key, since arrays compare by identity and every crossing copies, so a lookup could never succeed, nor as another array type (IntArray, Array<T>). It binds as an extension receiver (this byte[], nullable too), as a suspend parameter and result and a Flow element (see Coroutines and Flow).
Any
Any and Any? are object? wherever they appear: a parameter, result, property, List or Map element, lambda payload or result, or Flow element. A Kotlin value reaches C# as its runtime C# value, so test it with is or a pattern. A C# object reaches Kotlin as the matching Kotlin value.
Kotlin value | C# value |
|---|---|
| the boxed primitive |
|
|
an exported class | its wrapper, which you dispose ( |
a value class, an enum | the struct, the C# enum |
a |
|
an object a C# class implemented | the same C# object you handed over |
a generic value class ( | its struct closed over |
any other Kotlin object | a |
Going the other way, a list or dictionary you pass becomes a Kotlin List or Map with each element converted the same way, and a C# object Kotlin has no type for stays opaque inside Kotlin and comes back as the same instance.
A wrapper you receive as object? is yours to dispose, as at any other result position, and a wrapper you pass in is only borrowed for the call. A generic value class comes back closed over object? (Bow<object?>) whatever it was constructed as, so match on that.
A star projection reads at the parameter's bound, or object? when it has none: Box<*> is Box<object?>, Flow<*> is KotlinFlow<object?>, and a generic sealed Outcome<*> is Outcome<object>. Any also stands in for a member's own type parameter on an interface a C# class implements, see Implementing a Kotlin interface in C#.
C# names
Function and property names use PascalCase: string() becomes String(). For named arguments, use the generated parameter name. C# keywords are escaped, such as @abstract; reserved names such as error and handle gain a trailing underscore (error_, handle_). value keeps its name on a parameter. Positional calls are unaffected.
The same applies to errorOut and valueOut, on plain and generic functions alike:
Any other parameter name on a generic function is kept as written, including owned, result, width, and present.
Your own parameter names are kept even when they match a name the generator would otherwise use internally for a sibling parameter, such as a boolean flag next to a nullable value or a body local of a defaulted parameter's dispatch:
The one visible exception is a suspend parameter literally named cancellationToken: the user's name wins, and the generated cancellation token becomes cancellationToken_.