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 Rodriguez
• API & Security EngineerMichael 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.
# 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
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!
Related Resources
Related Articles
JSON APIs and REST Services: Complete Development Guide
Learn to build and consume JSON-based REST APIs. Covers HTTP methods, authentication, best practices, and real-world implementation examples.
How to Parse Large JSON Files Without Crashing: Complete Guide 2026
Learn how to parse 100MB+ JSON files without memory errors or browser crashes. Practical solutions with streaming, chunking, and optimization techniques for JavaScript, Python, and Node.js.
JSON Security Vulnerabilities: Complete Protection Guide 2026
Protect your APIs from JSON security vulnerabilities. Learn about injection attacks, prototype pollution, DoS attacks, and implement security best practices for safe JSON processing in production applications.