diff --git a/app/src/main/java/io/github/ryunen344/suburi/navigation/NavType.kt b/app/src/main/java/io/github/ryunen344/suburi/navigation/NavType.kt
new file mode 100644
index 0000000..bb15cdd
--- /dev/null
+++ b/app/src/main/java/io/github/ryunen344/suburi/navigation/NavType.kt
@@ -0,0 +1,132 @@
+package io.github.ryunen344.suburi.navigation
+
+import android.os.Bundle
+import androidx.core.os.BundleCompat
+import androidx.navigation.NavType
+import io.github.ryunen344.suburi.util.deserialize
+import io.github.ryunen344.suburi.util.serialize
+import timber.log.Timber
+import java.util.Base64
+import kotlin.reflect.KType
+import kotlin.reflect.typeOf
+
+@Suppress("FunctionName")
+inline fun <reified T : java.io.Serializable> SerializableNavTypeMap(): Map<KType, SerializableNavType<T>> =
+    mapOf(typeOf<T>() to SerializableNavType<T>())
+
+@Suppress("FunctionName")
+inline fun <reified T : java.io.Serializable> SerializableNavTypeMap(
+    noinline onParseValue: ((String) -> T?),
+): Map<KType, SerializableNavType<T>> = mapOf(typeOf<T>() to SerializableNavType<T>(onParseValue))
+
+inline fun <reified T : java.io.Serializable> SerializableNavType() = SerializableNavType(T::class.java)
+
+inline fun <reified T : java.io.Serializable> SerializableNavType(noinline onParseValue: ((String) -> T?)) =
+    SerializableNavType(T::class.java, onParseValue)
+
+class SerializableNavType<T : java.io.Serializable>(
+    val clazz: Class<T>,
+    private val onParseValue: ((String) -> T?)? = null,
+) : NavType<T>(false) {
+
+    override val name: String
+        get() = clazz.name
+
+    override fun put(bundle: Bundle, key: String, value: T) {
+        bundle.putSerializable(key, value)
+    }
+
+    override fun get(bundle: Bundle, key: String): T? {
+        return BundleCompat.getSerializable(bundle, key, clazz)
+    }
+
+    override fun parseValue(value: String): T {
+        Timber.d("parseValue $value")
+        return onParseValue?.invoke(value) ?: Base64.getUrlDecoder().decode(value).deserialize()
+    }
+
+    override fun serializeAsValue(value: T): String {
+        Timber.d("serializeAsValue $value")
+        return Base64.getUrlEncoder().encodeToString(value.serialize())
+    }
+
+    override fun equals(other: Any?): Boolean {
+        if (this === other) return true
+        if (javaClass != other?.javaClass) return false
+
+        other as SerializableNavType<*>
+
+        if (clazz != other.clazz) return false
+        if (onParseValue != other.onParseValue) return false
+
+        return true
+    }
+
+    override fun hashCode(): Int {
+        var result = clazz.hashCode()
+        result = 31 * result + (onParseValue?.hashCode() ?: 0)
+        return result
+    }
+}
+
+@Suppress("FunctionName")
+inline fun <reified T : java.io.Serializable> NullableSerializableNavTypeMap(): Map<KType, NullableSerializableNavType<T>> =
+    mapOf(typeOf<T>() to NullableSerializableNavType<T>())
+
+@Suppress("FunctionName")
+inline fun <reified T : java.io.Serializable> NullableSerializableNavTypeMap(
+    noinline onParseValue: ((String) -> T?),
+): Map<KType, NullableSerializableNavType<T>> = mapOf(typeOf<T>() to NullableSerializableNavType<T>(onParseValue))
+
+inline fun <reified T : java.io.Serializable> NullableSerializableNavType() = NullableSerializableNavType(T::class.java)
+
+inline fun <reified T : java.io.Serializable> NullableSerializableNavType(noinline onParseValue: ((String) -> T?)) =
+    NullableSerializableNavType(T::class.java, onParseValue)
+
+class NullableSerializableNavType<T : java.io.Serializable>(
+    val clazz: Class<T>,
+    private val onParseValue: ((String) -> T?)? = null,
+) : NavType<T?>(true) {
+
+    override val name: String
+        get() = clazz.name
+
+    override fun put(bundle: Bundle, key: String, value: T?) {
+        bundle.putSerializable(key, value)
+    }
+
+    override fun get(bundle: Bundle, key: String): T? {
+        return BundleCompat.getSerializable(bundle, key, clazz)
+    }
+
+    override fun parseValue(value: String): T? {
+        return if (value == "null") null else onParseValue?.invoke(value) ?: Base64.getUrlDecoder().decode(value).deserialize()
+    }
+
+    override fun serializeAsValue(value: T?): String {
+        Timber.d("serializeAsValue $value")
+        return if (value == null) {
+            "null"
+        } else {
+            Base64.getUrlEncoder().encodeToString(value.serialize())
+        }
+    }
+
+    override fun equals(other: Any?): Boolean {
+        if (this === other) return true
+        if (javaClass != other?.javaClass) return false
+
+        other as NullableSerializableNavType<*>
+
+        if (clazz != other.clazz) return false
+        if (onParseValue != other.onParseValue) return false
+
+        return true
+    }
+
+    override fun hashCode(): Int {
+        var result = clazz.hashCode()
+        result = 31 * result + (onParseValue?.hashCode() ?: 0)
+        return result
+    }
+}
diff --git a/app/src/main/java/io/github/ryunen344/suburi/ui/screen/Routes.kt b/app/src/main/java/io/github/ryunen344/suburi/ui/screen/Routes.kt
index 2b1e305..b171f32 100644
--- a/app/src/main/java/io/github/ryunen344/suburi/ui/screen/Routes.kt
+++ b/app/src/main/java/io/github/ryunen344/suburi/ui/screen/Routes.kt
@@ -1,6 +1,5 @@
 package io.github.ryunen344.suburi.ui.screen
 
-import android.net.Uri
 import android.os.Bundle
 import androidx.compose.animation.AnimatedContentScope
 import androidx.compose.animation.AnimatedContentTransitionScope
@@ -8,7 +7,6 @@ import androidx.compose.animation.EnterTransition
 import androidx.compose.animation.ExitTransition
 import androidx.compose.animation.SizeTransform
 import androidx.compose.runtime.Composable
-import androidx.core.os.BundleCompat
 import androidx.lifecycle.SavedStateHandle
 import androidx.navigation.NavBackStackEntry
 import androidx.navigation.NavDeepLink
@@ -20,6 +18,7 @@ import androidx.navigation.get
 import androidx.navigation.internalToRoute
 import androidx.navigation.navDeepLink
 import androidx.navigation.serialization.decodeArguments
+import io.github.ryunen344.suburi.navigation.SerializableNavTypeMap
 import kotlinx.serialization.KSerializer
 import kotlinx.serialization.Serializable
 import kotlinx.serialization.descriptors.PrimitiveKind
@@ -29,13 +28,20 @@ import kotlinx.serialization.encoding.Decoder
 import kotlinx.serialization.encoding.Encoder
 import kotlinx.serialization.serializer
 import timber.log.Timber
-import java.io.ByteArrayOutputStream
-import java.io.ObjectInputStream
-import java.io.ObjectOutputStream
 import java.util.UUID
 import kotlin.reflect.KClass
 import kotlin.reflect.KType
-import kotlin.reflect.typeOf
+
+val onWrappedUuidParse: ((String) -> WrappedUuid?) = { value ->
+    runCatching {
+        UUID.fromString(value)
+            .let(::WrappedUuid)
+            .also { Timber.d("from Deeplink $it") }
+    }.getOrElse {
+        Timber.d("from Navigation")
+        null
+    }
+}
 
 @Serializable
 sealed class Routes {
@@ -52,8 +58,8 @@ sealed class Routes {
 inline val <reified T : Routes> KClass<T>.typeMap: Map<KType, @JvmSuppressWildcards NavType<*>>
     get() = when (this) {
         Routes.Top::class -> emptyMap()
-        Routes.Uuid::class -> WrappedUuid.typeMap
-        Routes.Structures::class -> Structure.typeMap
+        Routes.Uuid::class -> SerializableNavTypeMap<WrappedUuid>(onParseValue = onWrappedUuidParse)
+        Routes.Structures::class -> SerializableNavTypeMap<Structure>()
         else -> error("unexpected type parameter")
     }
 
@@ -61,7 +67,10 @@ val <T : Routes> KClass<T>.deepLinks: List<NavDeepLink>
     get() = when (this) {
         Routes.Top::class -> emptyList()
         Routes.Uuid::class -> listOf(
-            navDeepLink<Routes.Uuid>(basePath = "https://www.example.com/uuid", typeMap = WrappedUuid.typeMap),
+            navDeepLink<Routes.Uuid>(
+                basePath = "https://www.example.com/uuid",
+                typeMap = SerializableNavTypeMap<WrappedUuid>(onParseValue = onWrappedUuidParse),
+            ),
         )
 
         Routes.Structures::class -> emptyList()
@@ -69,78 +78,11 @@ val <T : Routes> KClass<T>.deepLinks: List<NavDeepLink>
     }
 
 @Serializable
-data class WrappedUuid(@Serializable(with = UUIDSerializer::class) val value: UUID) : java.io.Serializable {
-    companion object {
-        val navType = object : NavType<WrappedUuid>(false) {
-            override fun get(bundle: Bundle, key: String): WrappedUuid? {
-                return BundleCompat.getSerializable(bundle, key, WrappedUuid::class.java)
-            }
-
-            override fun parseValue(value: String): WrappedUuid {
-                Timber.d("parseValue $value")
-                return runCatching {
-                    UUID.fromString(value)
-                        .let(::WrappedUuid)
-                        .also { Timber.d("from Deeplink $it") }
-                }.getOrElse {
-                    Timber.d("from Navigation")
-                    Uri.decode(value)
-                        .hexToByteArray()
-                        .inputStream()
-                        .use(::ObjectInputStream)
-                        .use(ObjectInputStream::readObject) as WrappedUuid
-                }
-            }
-
-            override fun put(bundle: Bundle, key: String, value: WrappedUuid) {
-                bundle.putSerializable(key, value)
-            }
-
-            override fun serializeAsValue(value: WrappedUuid): String {
-                Timber.d("serializeAsValue $value")
-                val hex = ByteArrayOutputStream().apply {
-                    use(::ObjectOutputStream).use { it.writeObject(value) }
-                }.toByteArray().toHexString()
-                return Uri.encode(hex)
-            }
-        }
-
-        val typeMap = mapOf(typeOf<WrappedUuid>() to navType)
-    }
-}
+data class WrappedUuid(@Serializable(with = UUIDSerializer::class) val value: UUID) : java.io.Serializable
 
 @Serializable
 data class Structure(val value1: String, val value2: Long, val value3: String) : java.io.Serializable {
     companion object {
-        val navType = object : NavType<Structure>(false) {
-            override fun get(bundle: Bundle, key: String): Structure? {
-                return BundleCompat.getSerializable(bundle, key, Structure::class.java)
-            }
-
-            override fun parseValue(value: String): Structure {
-                Timber.d("parseValue $value")
-                return Uri.decode(value)
-                    .hexToByteArray()
-                    .inputStream()
-                    .use(::ObjectInputStream)
-                    .use(ObjectInputStream::readObject) as Structure
-            }
-
-            override fun put(bundle: Bundle, key: String, value: Structure) {
-                bundle.putSerializable(key, value)
-            }
-
-            override fun serializeAsValue(value: Structure): String {
-                Timber.d("serializeAsValue $value")
-                val hex = ByteArrayOutputStream().apply {
-                    use(::ObjectOutputStream).use { it.writeObject(value) }
-                }.toByteArray().toHexString()
-                return Uri.encode(hex)
-            }
-        }
-
-        val typeMap = mapOf(typeOf<Structure>() to navType)
-
         fun random(): Structure {
             return Structure(
                 value1 = UUID.randomUUID().toString(),
diff --git a/app/src/main/java/io/github/ryunen344/suburi/util/Serializable.kt b/app/src/main/java/io/github/ryunen344/suburi/util/Serializable.kt
new file mode 100644
index 0000000..012dbc5
--- /dev/null
+++ b/app/src/main/java/io/github/ryunen344/suburi/util/Serializable.kt
@@ -0,0 +1,19 @@
+package io.github.ryunen344.suburi.util
+
+import java.io.ByteArrayInputStream
+import java.io.ByteArrayOutputStream
+import java.io.ObjectInputStream
+import java.io.ObjectOutputStream
+import java.io.Serializable
+
+fun Serializable.serialize(): ByteArray {
+    return ByteArrayOutputStream(DEFAULT_BUFFER_SIZE).use { byteArrayOutputStream ->
+        ObjectOutputStream(byteArrayOutputStream).use { it.writeObject(this) }
+        byteArrayOutputStream.toByteArray()
+    }
+}
+
+@Suppress("UNCHECKED_CAST")
+fun <T : Serializable> ByteArray.deserialize(): T {
+    return ObjectInputStream(ByteArrayInputStream(this)).use(ObjectInputStream::readObject) as T
+}