1.7.1
Added
-
Custom JSON Serialization (#19)
KSafeConfig now accepts a json parameter — a fully configured Json instance used for all user-payload serialization. This enables support for @Contextual types (e.g., UUID, Instant, BigDecimal) and custom SerializersModule registration.
val customJson = Json {
ignoreUnknownKeys = true
serializersModule = SerializersModule {
contextual(UUIDSerializer)
contextual(InstantSerializer)
}
}
val ksafe = KSafe(
config = KSafeConfig(json = customJson)
)- Serializers are registered once at the instance level and apply to all operations (
putDirect,getDirect,put,get,getFlow, delegates) - Internal metadata serialization is unaffected — it uses its own private codec
- Default remains
Json { ignoreUnknownKeys = true }viaKSafeDefaults.json— no changes needed for existing code kotlinx-serialization-jsonis declared in the library as a transitive dependency (apiscope) — no need to add it manually in your project- Note: Changing the
Jsonconfiguration for an existingfileNamenamespace may make previously stored non-primitive values unreadable
Sample Usage
Define custom serializers (I add two to show approaching n custom fields)
object UUIDSerializer : KSerializer<UUID> {
override val descriptor = PrimitiveSerialDescriptor("UUID", PrimitiveKind.STRING)
override fun serialize(encoder: Encoder, value: UUID) = encoder.encodeString(value.toString())
override fun deserialize(decoder: Decoder): UUID = UUID.fromString(decoder.decodeString())
}
object InstantSerializer : KSerializer<Instant> {
override val descriptor = PrimitiveSerialDescriptor("Instant", PrimitiveKind.STRING)
override fun serialize(encoder: Encoder, value: Instant) = encoder.encodeString(value.toString())
override fun deserialize(decoder: Decoder): Instant = Instant.parse(decoder.decodeString())
}Build a Json instance and register all your serializers in one place
val customJson = Json {
ignoreUnknownKeys = true
serializersModule = SerializersModule {
contextual(UUIDSerializer)
contextual(InstantSerializer)
// add as many as you need
}
}Pass it via KSafeConfig
val ksafe = KSafe(
context = context, // Android; omit on JVM/iOS/WASM
config = KSafeConfig(json = customJson)
)Use @contextual types directly, so no extra work at the call site
@Serializable
data class UserProfile(
val name: String,
@Contextual val id: UUID,
@Contextual val createdAt: Instant
)
ksafe.putDirect("profile", UserProfile("Alice", UUID.randomUUID(), Instant.now()))
val profile: UserProfile = ksafe.getDirect("profile", defaultProfile)Fixed
-
WASM: Encrypted
mutableStateOfDelegates Return Defaults on Page Reload
Fixed a race condition on WASM where mutableStateOf Compose delegates could return the default value instead of the persisted encrypted value after a browser refresh. This occurred because WASM's WebCrypto decryption is async-only — if a KSafe instance was created and immediately read from in the same synchronous frame (e.g., via Koin lazy singleton injection into a ViewModel), the cache hadn't loaded yet.
The fix adds reactive self-healing to KSafeComposeState: when getDirect returns the default, a lightweight coroutine observes getFlow and updates the Compose state when the real decrypted value arrives. A userHasWritten guard ensures user writes are never overwritten by late-arriving cache data.
This bug was latent since WASM support was added but only surfaced when using multiple KSafe instances (e.g., a second instance with custom JSON serialization), where the second instance had no head start for its async cache loading.
-
Inline Bytecode Bloat (#16)
Reduced bytecode generated at each KSafe call site by extracting non-reified logic from inline functions into @PublishedApi internal helpers. Previously, every getDirect/putDirect delegate expansion could produce thousands of bytecode instructions because the entire function body was inlined. Now only the serializer<T>() call is inlined; the rest is a regular function call to the *Raw variant.
-
Relaxed
fileNameValidation
The fileName parameter now accepts lowercase letters, digits, and underscores (must start with a letter). Previously only [a-z]+ was allowed, which was unnecessarily restrictive. The regex is now [a-z][a-z0-9_]* across all platforms. Dots, slashes, and uppercase remain forbidden to prevent path traversal and case-sensitivity issues.
Full Changelog: 1.7.0...1.7.1