Shared Models
Communityahmed3elshaer
Shared data models for Kotlin Multiplatform using kotlinx.serialization. Cross-platform domain models with validation and serialization.
Overview
- Repository
- ahmed3elshaer/everything-claude-code-mobile
- Publisher
- ahmed3elshaer
- Stars
- 66
- Forks
- 18
- Bundled files
- 0
- License
- MIT
- Updated
- Jun 14, 2026
Installation
tmp=$(mktemp -d)
git clone --depth 1 https://github.com/ahmed3elshaer/everything-claude-code-mobile.git "$tmp"
mkdir -p ~/.claude/skills
cp -R "$tmp/skills/shared-models" ~/.claude/skills/shared-models
rm -rf "$tmp"tmp=$(mktemp -d)
git clone --depth 1 https://github.com/ahmed3elshaer/everything-claude-code-mobile.git "$tmp"
mkdir -p ~/.codex/skills
cp -R "$tmp/skills/shared-models" ~/.codex/skills/shared-models
rm -rf "$tmp"Install the Shared Models agent skill from https://github.com/ahmed3elshaer/everything-claude-code-mobile/tree/main/skills/shared-models into this project's skills folder.Instructions
Shared Models for KMP
Design and implement data models that work across all platforms in shared/commonMain.
Core Dependencies
// build.gradle.kts (shared module)
sourceSets {
val commonMain by getting {
dependencies {
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.6.0")
implementation("org.jetbrains.kotlinx:kotlinx-datetime:1.6.0")
}
}
}Enable serialization plugin:
plugins {
kotlin("multiplatform")
kotlin("plugin.serialization") version "1.9.20"
}Domain Models
1. Immutable Data Classes
// commonMain/kotlin/com/example/shared/model/User.kt
@Serializable
data class User(
val id: String,
val name: String,
val email: String,
val avatarUrl: String?,
val createdAt: Instant,
val lastActiveAt: Instant?
)2. Sealed Hierarchies
// commonMain/kotlin/com/example/shared/model/UiState.kt
@Serializable
sealed class UiState<out T> {
@Serializable
data object Loading : UiState<Nothing>()
@Serializable
data class Success<T>(val data: T) : UiState<T>()
@Serializable
data class Error(val message: String, val code: String? = null) : UiState<Nothing>()
}
// Usage with type parameter
@Serializable
sealed class HomeState {
@Serializable
data object Loading : HomeState()
@Serializable
data class Loaded(val user: User, val items: List<Item>) : HomeState()
@Serializable
data class Error(val message: String) : HomeState()
}3. Result Wrapper
// commonMain/kotlin/com/example/shared/model/Result.kt
@Serializable
sealed class Result<out T> {
@Serializable
data class Success<T>(val data: T) : Result<T>()
@Serializable
data class Error(val code: String, val message: String) : Result<Nothing>()
}
// Helper to convert from Kotlin Result
fun <T> Result<T>.toKotlinResult(): kotlin.Result<T> = when (this) {
is Result.Success -> kotlin.Result.success(data)
is Result.Error -> kotlin.Result.failure(RuntimeException("$code: $message"))
}4. Paginated Response
// commonMain/kotlin/com/example/shared/model/Pagination.kt
@Serializable
data class PaginatedResponse<T>(
val items: List<T>,
val page: Int,
val pageSize: Int,
val totalPages: Int,
val totalItems: Long
) {
val hasMorePages: Boolean get() = page < totalPages
val nextPage: Int? get() = if (hasMorePages) page + 1 else null
}
// For cursor-based pagination
@Serializable
data class CursorResponse<T>(
val items: List<T>,
val nextCursor: String?,
val hasMore: Boolean
)5. Request/Response Models
// commonMain/kotlin/com/example/shared/model/auth/AuthRequests.kt
@Serializable
data class LoginRequest(
val email: String,
val password: String
)
@Serializable
data class RegisterRequest(
val name: String,
val email: String,
val password: String
)
// commonMain/kotlin/com/example/shared/model/auth/AuthResponses.kt
@Serializable
data class AuthResponse(
val user: User,
val accessToken: String,
val refreshToken: String,
val expiresAt: Instant
)
@Serializable
data class RefreshTokenRequest(
val refreshToken: String
)Validation
Inline Validation
// commonMain/kotlin/com/example/shared/model/Validation.kt
@Serializable
data class Email(val value: String) {
init {
require(value.contains("@")) { "Invalid email format" }
require(value.length > 5) { "Email too short" }
}
companion object {
fun of(value: String?): Email? {
return if (!value.isNullOrBlank()) Email(value) else null
}
}
}
@Serializable
data class PhoneNumber(val value: String) {
init {
require(value.matches(Regex("^\\+?[1-9]\\d{1,14}$"))) {
"Invalid phone number format"
}
}
}Validation Result
// commonMain/kotlin/com/example/shared/model/ValidationError.kt
@Serializable
data class ValidationError(
val field: String,
val message: String
)
@Serializable
data class ValidationResult(
val isValid: Boolean,
val errors: List<ValidationError> = emptyList()
) {
companion object {
fun success() = ValidationResult(isValid = true)
fun failure(errors: List<ValidationError>) = ValidationResult(
isValid = false,
errors = errors
)
}
}
// Usage in models
@Serializable
data class CreateUserRequest(
val name: String,
val email: String,
val age: Int?
) {
fun validate(): ValidationResult {
val errors = buildList {
if (name.isBlank()) {
add(ValidationError("name", "Name is required"))
}
if (email.isBlank() || !email.contains("@")) {
add(ValidationError("email", "Invalid email"))
}
if (age != null && age < 0) {
add(ValidationError("age", "Age cannot be negative"))
}
}
return if (errors.isEmpty()) ValidationResult.success()
else ValidationResult.failure(errors)
}
}Platform-Specific Fields
Using Serial Names
// commonMain/kotlin/com/example/shared/model/PlatformData.kt
@Serializable
data class PlatformData(
val platform: Platform,
val deviceInfo: DeviceInfo
)
@Serializable
enum class Platform {
ANDROID,
IOS,
DESKTOP,
WEB
}
@Serializable
data class DeviceInfo(
val model: String,
val osVersion: String,
val appVersion: String,
// Platform-specific optional fields
val pushToken: String? = null,
val advertisingId: String? = null
)Custom Serializers
// commonMain/kotlin/com/example/shared/model/InstantSerializer.kt
object InstantSerializer : KSerializer<Instant> {
override val descriptor: SerialDescriptor =
PrimitiveSerialDescriptor("Instant", PrimitiveKind.LONG)
override fun serialize(encoder: Encoder, value: Instant) {
encoder.encodeLong(value.toEpochMilliseconds())
}
override fun deserialize(decoder: Decoder): Instant {
return Instant.fromEpochMilliseconds(decoder.decodeLong())
}
}
@Serializable
data class Event(
val id: String,
@Serializable(with = InstantSerializer::class)
val timestamp: Instant
)JSON Configuration
// commonMain/kotlin/com/example/shared/serialization/JsonFactory.kt
object JsonFactory {
val Default = Json {
ignoreUnknownKeys = true
isLenient = true
encodeDefaults = false
coerceInputValues = true
}
// Pretty printing for debug
val Pretty = Json {
ignoreUnknownKeys = true
isLenient = true
prettyPrint = true
indent = " "
}
// Strict parsing for API responses
val Strict = Json {
ignoreUnknownKeys = false
isLenient = false
encodeDefaults = false
coerceInputValues = false
}
}File Organization
shared/commonMain/kotlin/com/example/shared/
├── model/
│ ├── User.kt
│ ├── Item.kt
│ ├── Pagination.kt
│ ├── UiState.kt
│ └── Result.kt
├── model/auth/
│ ├── AuthRequests.kt
│ ├── AuthResponses.kt
│ └── UserProfile.kt
├── serialization/
│ ├── JsonFactory.kt
│ └── InstantSerializer.kt
└── validation/
├── ValidationResult.kt
└── Validators.kt
Best Practices
✅ DO
// ✅ Use immutable data classes
@Serializable
data class User(val id: String, val name: String)
// ✅ Use sealed classes for fixed types
@Serializable
sealed class Result
// ✅ Provide default values for optional fields
@Serializable
data class Item(
val id: String,
val description: String? = null
)
// ✅ Use value classes for type safety
@JvmInline
@Serializable
value class UserId(val value: String)
// ✅ Group related models in packages
model/
auth/
payment/
social/❌ DON'T
// ❌ Don't use platform-specific types
@Serializable
data class Event(val date: Date) // Date is platform-specific
// Use Instant or LocalDateTime instead
// ❌ Don't include complex logic in models
@Serializable
data class User(val id: String) {
// Heavy business logic doesn't belong here
fun calculateSomethingComplex(): Int { ... }
}
// ❌ Don't make everything nullable
@Serializable
data class Item(
val id: String?,
val name: String?,
val price: Double?
) // Use Optional pattern or separate fields
// ❌ Don't use var in data classes
@Serializable
data class User(var name: String) // Use val for immutabilityTesting
// commonTest/kotlin/ModelTest.kt
class ModelTest {
@Test
fun `serialize and deserialize user`() {
val user = User(
id = "123",
name = "John Doe",
email = "[email protected]",
avatarUrl = null,
createdAt = Clock.System.now(),
lastActiveAt = null
)
val json = JsonFactory.Default.encodeToString(user)
val restored = JsonFactory.Default.decodeFromString<User>(json)
assertEquals(user, restored)
}
@Test
fun `validation catches invalid email`() {
val result = CreateUserRequest(
name = "John",
email = "not-an-email",
age = null
).validate()
assertFalse(result.isValid)
assertTrue(result.errors.any { it.field == "email" })
}
}Remember: Shared models are your contract between platforms. Keep them simple, immutable, and focused on data.
FAQ
What does Shared Models do?
Shared data models for Kotlin Multiplatform using kotlinx.serialization. Cross-platform domain models with validation and serialization.
Who made Shared Models?
ahmed3elshaer (a community member) publishes it in the ahmed3elshaer/everything-claude-code-mobile repository on GitHub.
Is Shared Models free?
Yes. Shared Models is open source, published by ahmed3elshaer under the MIT license.
What comes with Shared Models?
Just its instructions. Everything the agent needs is in one SKILL.md file.
Which agents can use Shared Models?
Any coding agent that reads SKILL.md files, including Claude Code, Codex, Cursor, GitHub Copilot and Gemini CLI. The agent loads the skill when a task matches its description.
What are agent skills?
An agent skill is a folder with a SKILL.md file: instructions that teach a coding agent how to do one kind of task, and optionally files it can read while it works. The agent keeps only the skill's name and description in mind and reads the rest when a task calls for it.
How are agent skills different from MCP servers?
An MCP server gives an agent new tools to call, code that runs somewhere and returns results. A skill gives it knowledge: how to approach a task with the tools it already has. Many agents use both.
How can my agent test my Android app?
Robomori lets coding agents create, start and control Android emulators and devices from one command, so they can install your app, take screenshots and check dark mode, font sizes and languages.