Skip to content

1.7.1

Choose a tag to compare

@ioannisa ioannisa released this 17 Mar 14:47

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 } via KSafeDefaults.json — no changes needed for existing code
  • kotlinx-serialization-json is declared in the library as a transitive dependency (api scope) — no need to add it manually in your project
  • Note: Changing the Json configuration for an existing fileName namespace 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 mutableStateOf Delegates 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 fileName Validation

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