← Back to Blog

JSON in Mobile Apps: iOS Swift & Android Kotlin Best Practices 2026

Complete guide to JSON handling in mobile development. Learn parsing, serialization, networking, and offline storage for iOS (Swift/SwiftUI) and Android (Kotlin/Jetpack Compose) apps.

Michael Rodriguez14 min readtutorial
M

Michael Rodriguez

API & Security Engineer

Michael is an API engineer and security specialist with over 7 years of experience building RESTful services, data conversion pipelines, and authentication systems. He writes practical guides on JSON Web Tokens, API debugging strategies, data science applications of JSON, and modern AI tooling workflows including MCP and JSON-RPC.

REST APIsJWT & SecurityData ScienceJSON PathMCP / AI ToolingAPI Debugging
14 min read

# JSON in Mobile Apps: iOS Swift & Android Kotlin Best Practices 2026

Mobile apps rely heavily on JSON for API communication, local caching, and configuration. This guide covers modern JSON handling in Swift (iOS) and Kotlin (Android) with production-ready patterns.

Table of Contents

  • iOS: JSON with Swift & Codable
  • iOS: Networking with URLSession
  • iOS: SwiftUI Integration
  • Android: JSON with Kotlin & kotlinx.serialization
  • Android: Retrofit & Networking
  • Android: Jetpack Compose Integration
  • Offline Storage & Caching
  • Performance & Security
  • ---

    iOS: JSON with Swift & Codable

    Basic Codable Implementation

    import Foundation
    
    

    struct User: Codable {

    let id: Int

    let name: String

    let email: String

    let isActive: Bool

    // Custom key mapping

    enum CodingKeys: String, CodingKey {

    case id

    case name

    case email

    case isActive = "is_active"

    }

    }

    // Decode JSON

    let jsonString = """

    {

    "id": 1,

    "name": "Alice",

    "email": "alice@example.com",

    "is_active": true

    }

    """

    let jsonData = jsonString.data(using: .utf8)!

    do {

    let user = try JSONDecoder().decode(User.self, from: jsonData)

    print(user.name) // "Alice"

    } catch {

    print("Failed to decode: \(error)")

    }

    // Encode to JSON

    let user = User(id: 1, name: "Alice", email: "alice@example.com", isActive: true)

    do {

    let encoder = JSONEncoder()

    encoder.outputFormatting = .prettyPrinted

    let jsonData = try encoder.encode(user)

    if let jsonString = String(data: jsonData, encoding: .utf8) {

    print(jsonString)

    }

    } catch {

    print("Failed to encode: \(error)")

    }

    Nested JSON Objects

    struct Post: Codable {
    

    let id: Int

    let title: String

    let author: Author

    let tags: [String]

    let metadata: Metadata

    }

    struct Author: Codable {

    let id: Int

    let name: String

    }

    struct Metadata: Codable {

    let views: Int

    let likes: Int

    }

    let jsonString = """

    {

    "id": 1,

    "title": "Hello World",

    "author": {

    "id": 10,

    "name": "Alice"

    },

    "tags": ["swift", "ios"],

    "metadata": {

    "views": 1000,

    "likes": 50

    }

    }

    """

    let post = try JSONDecoder().decode(Post.self, from: jsonString.data(using: .utf8)!)

    print(post.author.name) // "Alice"

    Optional & Default Values

    struct Product: Codable {
    

    let id: Int

    let name: String

    let price: Double

    let description: String? // Optional

    let inStock: Bool = true // Default value

    init(from decoder: Decoder) throws {

    let container = try decoder.container(keyedBy: CodingKeys.self)

    id = try container.decode(Int.self, forKey: .id)

    name = try container.decode(String.self, forKey: .name)

    price = try container.decode(Double.self, forKey: .price)

    description = try container.decodeIfPresent(String.self, forKey: .description)

    inStock = try container.decodeIfPresent(Bool.self, forKey: .inStock) ?? true

    }

    }

    Date Handling

    struct Event: Codable {
    

    let id: Int

    let name: String

    let startDate: Date

    }

    let jsonString = """

    {

    "id": 1,

    "name": "Conference",

    "start_date": "2026-03-20T10:00:00Z"

    }

    """

    // Configure decoder for ISO8601 dates

    let decoder = JSONDecoder()

    decoder.keyDecodingStrategy = .convertFromSnakeCase

    decoder.dateDecodingStrategy = .iso8601

    let event = try decoder.decode(Event.self, from: jsonString.data(using: .utf8)!)

    print(event.startDate) // Date object

    // Custom date format

    let customDecoder = JSONDecoder()

    let formatter = DateFormatter()

    formatter.dateFormat = "yyyy-MM-dd"

    customDecoder.dateDecodingStrategy = .formatted(formatter)

    ---

    iOS: Networking with URLSession

    Basic API Call

    func fetchUsers() async throws -> [User] {
    

    let url = URL(string: "https://api.example.com/users")!

    let (data, response) = try await URLSession.shared.data(from: url)

    guard let httpResponse = response as? HTTPURLResponse,

    (200...299).contains(httpResponse.statusCode) else {

    throw URLError(.badServerResponse)

    }

    let decoder = JSONDecoder()

    let users = try decoder.decode([User].self, from: data)

    return users

    }

    // Usage with async/await

    Task {

    do {

    let users = try await fetchUsers()

    print("Fetched \(users.count) users")

    } catch {

    print("Error: \(error)")

    }

    }

    POST Request with JSON Body

    func createUser(name: String, email: String) async throws -> User {
    

    let url = URL(string: "https://api.example.com/users")!

    var request = URLRequest(url: url)

    request.httpMethod = "POST"

    request.setValue("application/json", forHTTPHeaderField: "Content-Type")

    let body: [String: Any] = [

    "name": name,

    "email": email

    ]

    request.httpBody = try JSONSerialization.data(withJSONObject: body)

    let (data, response) = try await URLSession.shared.data(for: request)

    guard let httpResponse = response as? HTTPURLResponse,

    (200...299).contains(httpResponse.statusCode) else {

    throw URLError(.badServerResponse)

    }

    return try JSONDecoder().decode(User.self, from: data)

    }

    API Response Wrapper

    struct ApiResponse<T: Codable>: Codable {
    

    let status: String

    let data: T?

    let error: String?

    }

    func fetchData<T: Codable>(from url: URL) async throws -> T {

    let (data, _) = try await URLSession.shared.data(from: url)

    let response = try JSONDecoder().decode(ApiResponse<T>.self, from: data)

    if response.status == "success", let data = response.data {

    return data

    } else {

    throw NSError(domain: "API", code: 0, userInfo: [

    NSLocalizedDescriptionKey: response.error ?? "Unknown error"

    ])

    }

    }

    // Usage

    let users: [User] = try await fetchData(from: URL(string: "https://api.example.com/users")!)

    ---

    iOS: SwiftUI Integration

    ObservableObject for State Management

    import SwiftUI
    
    

    class UserViewModel: ObservableObject {

    @Published var users: [User] = []

    @Published var isLoading = false

    @Published var errorMessage: String?

    func fetchUsers() {

    isLoading = true

    errorMessage = nil

    Task {

    do {

    let fetchedUsers = try await fetchUsersFromAPI()

    await MainActor.run {

    self.users = fetchedUsers

    self.isLoading = false

    }

    } catch {

    await MainActor.run {

    self.errorMessage = error.localizedDescription

    self.isLoading = false

    }

    }

    }

    }

    private func fetchUsersFromAPI() async throws -> [User] {

    let url = URL(string: "https://api.example.com/users")!

    let (data, _) = try await URLSession.shared.data(from: url)

    return try JSONDecoder().decode([User].self, from: data)

    }

    }

    struct UserListView: View {

    @StateObject private var viewModel = UserViewModel()

    var body: some View {

    NavigationView {

    Group {

    if viewModel.isLoading {

    ProgressView("Loading...")

    } else if let error = viewModel.errorMessage {

    Text("Error: \(error)")

    .foregroundColor(.red)

    } else {

    List(viewModel.users, id: \.id) { user in

    VStack(alignment: .leading) {

    Text(user.name)

    .font(.headline)

    Text(user.email)

    .font(.subheadline)

    .foregroundColor(.gray)

    }

    }

    }

    }

    .navigationTitle("Users")

    .onAppear {

    viewModel.fetchUsers()

    }

    }

    }

    }

    ---

    Android: JSON with Kotlin & kotlinx.serialization

    Setup

    // build.gradle.kts
    

    plugins {

    kotlin("plugin.serialization") version "1.9.0"

    }

    dependencies {

    implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.6.0")

    }

    Basic Serialization

    import kotlinx.serialization.
    

    import kotlinx.serialization.json.

    @Serializable

    data class User(

    val id: Int,

    val name: String,

    val email: String,

    @SerialName("is_active") val isActive: Boolean

    )

    // Decode JSON

    val jsonString = """

    {

    "id": 1,

    "name": "Alice",

    "email": "alice@example.com",

    "is_active": true

    }

    """

    val user = Json.decodeFromString<User>(jsonString)

    println(user.name) // "Alice"

    // Encode to JSON

    val user = User(1, "Alice", "alice@example.com", true)

    val json = Json.encodeToString(user)

    println(json)

    Nested Objects

    @Serializable
    

    data class Post(

    val id: Int,

    val title: String,

    val author: Author,

    val tags: List<String>,

    val metadata: Metadata

    )

    @Serializable

    data class Author(val id: Int, val name: String)

    @Serializable

    data class Metadata(val views: Int, val likes: Int)

    val jsonString = """

    {

    "id": 1,

    "title": "Hello World",

    "author": {"id": 10, "name": "Alice"},

    "tags": ["kotlin", "android"],

    "metadata": {"views": 1000, "likes": 50}

    }

    """

    val post = Json.decodeFromString<Post>(jsonString)

    println(post.author.name) // "Alice"

    Optional & Default Values

    @Serializable
    

    data class Product(

    val id: Int,

    val name: String,

    val price: Double,

    val description: String? = null, // Optional

    val inStock: Boolean = true // Default value

    )

    ---

    Android: Retrofit & Networking

    Setup

    // build.gradle.kts
    

    dependencies {

    implementation("com.squareup.retrofit2:retrofit:2.9.0")

    implementation("com.jakewharton.retrofit:retrofit2-kotlinx-serialization-converter:1.0.0")

    implementation("com.squareup.okhttp3:okhttp:4.11.0")

    implementation("com.squareup.okhttp3:logging-interceptor:4.11.0")

    }

    API Interface

    import retrofit2.http.
    
    

    interface ApiService {

    @GET("users")

    suspend fun getUsers(): List<User>

    @GET("users/{id}")

    suspend fun getUser(@Path("id") id: Int): User

    @POST("users")

    suspend fun createUser(@Body user: User): User

    @PUT("users/{id}")

    suspend fun updateUser(@Path("id") id: Int, @Body user: User): User

    @DELETE("users/{id}")

    suspend fun deleteUser(@Path("id") id: Int)

    }

    Retrofit Client

    import okhttp3.OkHttpClient
    

    import okhttp3.logging.HttpLoggingInterceptor

    import retrofit2.Retrofit

    import com.jakewharton.retrofit2.converter.kotlinx.serialization.asConverterFactory

    import kotlinx.serialization.json.Json

    import okhttp3.MediaType.Companion.toMediaType

    object RetrofitClient {

    private const val BASE_URL = "https://api.example.com/"

    private val json = Json {

    ignoreUnknownKeys = true

    isLenient = true

    }

    private val loggingInterceptor = HttpLoggingInterceptor().apply {

    level = HttpLoggingInterceptor.Level.BODY

    }

    private val client = OkHttpClient.Builder()

    .addInterceptor(loggingInterceptor)

    .build()

    val api: ApiService = Retrofit.Builder()

    .baseUrl(BASE_URL)

    .client(client)

    .addConverterFactory(json.asConverterFactory("application/json".toMediaType()))

    .build()

    .create(ApiService::class.java)

    }

    // Usage

    suspend fun fetchUsers(): List<User> {

    return try {

    RetrofitClient.api.getUsers()

    } catch (e: Exception) {

    emptyList()

    }

    }

    ---

    Android: Jetpack Compose Integration

    ViewModel with StateFlow

    import androidx.lifecycle.ViewModel
    

    import androidx.lifecycle.viewModelScope

    import kotlinx.coroutines.flow.

    import kotlinx.coroutines.launch

    class UserViewModel : ViewModel() {

    private val _uiState = MutableStateFlow<UiState>(UiState.Loading)

    val uiState: StateFlow<UiState> = _uiState.asStateFlow()

    init {

    fetchUsers()

    }

    fun fetchUsers() {

    viewModelScope.launch {

    _uiState.value = UiState.Loading

    try {

    val users = RetrofitClient.api.getUsers()

    _uiState.value = UiState.Success(users)

    } catch (e: Exception) {

    _uiState.value = UiState.Error(e.message ?: "Unknown error")

    }

    }

    }

    }

    sealed class UiState {

    object Loading : UiState()

    data class Success(val users: List<User>) : UiState()

    data class Error(val message: String) : UiState()

    }

    Compose UI

    import androidx.compose.runtime.
    

    import androidx.compose.foundation.layout.

    import androidx.compose.foundation.lazy.LazyColumn

    import androidx.compose.foundation.lazy.items

    import androidx.compose.material3.

    import androidx.lifecycle.viewmodel.compose.viewModel

    @Composable

    fun UserListScreen(viewModel: UserViewModel = viewModel()) {

    val uiState by viewModel.uiState.collectAsState()

    Scaffold(

    topBar = {

    TopAppBar(title = { Text("Users") })

    }

    ) { padding ->

    Box(

    modifier = Modifier

    .fillMaxSize()

    .padding(padding)

    ) {

    when (val state = uiState) {

    is UiState.Loading -> {

    CircularProgressIndicator(

    modifier = Modifier.align(Alignment.Center)

    )

    }

    is UiState.Success -> {

    LazyColumn {

    items(state.users) { user ->

    UserItem(user)

    }

    }

    }

    is UiState.Error -> {

    Text(

    text = "Error: ${state.message}",

    color = MaterialTheme.colorScheme.error,

    modifier = Modifier.align(Alignment.Center)

    )

    }

    }

    }

    }

    }

    @Composable

    fun UserItem(user: User) {

    Card(

    modifier = Modifier

    .fillMaxWidth()

    .padding(8.dp)

    ) {

    Column(modifier = Modifier.padding(16.dp)) {

    Text(text = user.name, style = MaterialTheme.typography.titleMedium)

    Text(text = user.email, style = MaterialTheme.typography.bodyMedium)

    }

    }

    }

    ---

    Offline Storage & Caching

    iOS: UserDefaults

    extension UserDefaults {
    

    func setEncodable<T: Encodable>(_ value: T, forKey key: String) {

    if let data = try? JSONEncoder().encode(value) {

    set(data, forKey: key)

    }

    }

    func getDecodable<T: Decodable>(_ type: T.Type, forKey key: String) -> T? {

    guard let data = data(forKey: key) else { return nil }

    return try? JSONDecoder().decode(type, from: data)

    }

    }

    // Usage

    UserDefaults.standard.setEncodable(user, forKey: "cachedUser")

    let cachedUser = UserDefaults.standard.getDecodable(User.self, forKey: "cachedUser")

    Android: DataStore

    // build.gradle.kts
    

    dependencies {

    implementation("androidx.datastore:datastore-preferences:1.0.0")

    }

    import androidx.datastore.core.DataStore

    import androidx.datastore.preferences.core.

    import kotlinx.coroutines.flow.map

    class UserPreferences(private val dataStore: DataStore<Preferences>) {

    private val USER_JSON = stringPreferencesKey("user_json")

    suspend fun saveUser(user: User) {

    val json = Json.encodeToString(user)

    dataStore.edit { preferences ->

    preferences[USER_JSON] = json

    }

    }

    val user = dataStore.data.map { preferences ->

    preferences[USER_JSON]?.let { json ->

    Json.decodeFromString<User>(json)

    }

    }

    }

    ---

    Performance & Security

    iOS: Streaming Large JSON

    let url = URL(string: "https://api.example.com/large-data")!
    

    let (asyncBytes, response) = try await URLSession.shared.bytes(from: url)

    var buffer = Data()

    for try await byte in asyncBytes {

    buffer.append(byte)

    // Process chunks as they arrive

    if buffer.count >= 1024 * 1024 { // 1MB chunks

    processChunk(buffer)

    buffer.removeAll()

    }

    }

    Android: HTTPS Enforced

    val client = OkHttpClient.Builder()
    

    .addInterceptor { chain ->

    val request = chain.request()

    if (request.url.scheme != "https") {

    throw SecurityException("HTTP connections not allowed")

    }

    chain.proceed(request)

    }

    .build()

    ---

    Conclusion

    Modern mobile JSON handling:

    iOS: Codable for type-safe parsing, URLSession for networking, SwiftUI for reactive UI Android: kotlinx.serialization for JSON, Retrofit for APIs, Jetpack Compose for UI Best practices:
    • Always validate JSON schema
    • Cache responses for offline support
    • Use HTTPS only
    • Handle errors gracefully
    • Test with large datasets

    Build robust, performant mobile apps with proper JSON handling!

    Share:

    Related Articles