JSON in Mobile Apps: iOS Codable, Android Gson, Flutter
Last updated:
Mobile apps parse JSON from REST APIs constantly — iOS uses Swift's Codable protocol with JSONDecoder, Android uses Gson or Moshi with Kotlin data classes, and Flutter uses dart:convert with fromJson/toJson factory methods. Swift's JSONDecoder parses a 100 KB JSON response in under 2 ms on iPhone 15; Gson parses the same payload in ~3 ms on Pixel 8. Both support custom date formats via dateDecodingStrategy / GsonBuilder().setDateFormat(). This guide covers JSON decode/encode in Swift (Codable), Kotlin (Gson and Moshi), and Dart/Flutter (fromJson/toJson), handling null fields, nested objects, and custom date formats in each platform. Internal links lead to the dedicated per-language deep dives: parse JSON in Swift, parse JSON in Kotlin, and parse JSON in Dart/Flutter.
Swift Codable: Decodable, Encodable, JSONDecoder, and CodingKeys
Swift's Codable protocol (a type alias for Decodable & Encodable) lets the compiler automatically synthesize JSON decode and encode logic for any struct or class whose stored properties are all themselves Codable. Use Decodable alone when you only need to parse; use Encodable when you only need to serialize. JSONDecoder is the standard Foundation class for parsing JSON Data into typed Swift types. Call try decoder.decode(User.self, from: data) — the decoder throws a structured DecodingError on any failure, so always wrap it in a do{ try ... } catch block in production code.
import Foundation
// 1. Define a struct conforming to Codable
struct User: Codable {
var id: Int
var name: String
var email: String
var isActive: Bool
}
// 2. Decode from a JSON string
let json = """
{
"id": 1,
"name": "Alice",
"email": "alice@example.com",
"is_active": true
}
"""
let decoder = JSONDecoder()
decoder.keyDecodingStrategy = .convertFromSnakeCase // maps is_active -> isActive
do {
let user = try decoder.decode(User.self, from: json.data(using: .utf8)!)
print(user.name) // Alice
print(user.isActive) // true
} catch let error as DecodingError {
print("Decode error:", error)
}
// 3. Encode back to JSON
let encoder = JSONEncoder()
encoder.outputFormatting = .prettyPrinted
encoder.keyEncodingStrategy = .convertToSnakeCase
let data = try! encoder.encode(user)
print(String(data: data, encoding: .utf8)!)
// {"id":1,"name":"Alice","email":"alice@example.com","is_active":true}
// 4. CodingKeys — fine-grained key remapping
struct Product: Codable {
var productId: Int
var productName: String
var unitPrice: Double
enum CodingKeys: String, CodingKey {
case productId = "product_id"
case productName = "product_name"
case unitPrice = "unit_price"
}
}
// 5. Optional fields — nil if key absent or value is null
struct Article: Codable {
var id: Int
var title: String
var subtitle: String? // nil when missing or null
var tags: [String]? // nil when missing or null
}For a complete deep dive including URLSession async/await and nested types, see the dedicated guide on parse JSON in Swift.
Swift JSON date formats: dateDecodingStrategy and custom DateFormatter
JSON has no native date type. Dates arrive as ISO 8601 strings, Unix timestamps (seconds or milliseconds), or custom format strings. Set decoder.dateDecodingStrategy before calling decode and declare the property as Date — Swift converts the raw JSON value automatically. The wrong strategy throws DecodingError.dataCorrupted. See the JSON date formats guide for a cross-platform comparison.
import Foundation
struct Event: Codable {
var id: Int
var name: String
var startsAt: Date
var createdAt: Date
}
// ── Strategy 1: ISO 8601 ("2026-05-19T17:00:00Z") ─────────────────────────
let isoDecoder = JSONDecoder()
isoDecoder.keyDecodingStrategy = .convertFromSnakeCase
isoDecoder.dateDecodingStrategy = .iso8601
let isoJson = """
{"id":1,"name":"WWDC","starts_at":"2026-06-09T17:00:00Z","created_at":"2026-05-19T08:00:00Z"}
"""
let event1 = try! isoDecoder.decode(Event.self, from: isoJson.data(using: .utf8)!)
print(event1.startsAt) // 2026-06-09 17:00:00 +0000
// ── Strategy 2: Unix timestamp in seconds ─────────────────────────────────
let unixDecoder = JSONDecoder()
unixDecoder.keyDecodingStrategy = .convertFromSnakeCase
unixDecoder.dateDecodingStrategy = .secondsSince1970
// ── Strategy 3: Unix timestamp in milliseconds ────────────────────────────
let msDecoder = JSONDecoder()
msDecoder.dateDecodingStrategy = .millisecondsSince1970
// ── Strategy 4: Custom DateFormatter (non-standard format) ────────────────
let fmt = DateFormatter()
fmt.dateFormat = "yyyy-MM-dd HH:mm:ss"
fmt.locale = Locale(identifier: "en_US_POSIX")
fmt.timeZone = TimeZone(identifier: "UTC")
let customDecoder = JSONDecoder()
customDecoder.keyDecodingStrategy = .convertFromSnakeCase
customDecoder.dateDecodingStrategy = .formatted(fmt)
// ── Strategy 5: ISO 8601 with fractional seconds or timezone offset ────────
// .iso8601 only handles the literal-Z suffix format.
// For "+05:30" offsets or ".000Z" fractional seconds, use ISO8601DateFormatter:
let iso8601Fmt = ISO8601DateFormatter()
iso8601Fmt.formatOptions = [.withInternetDateTime, .withFractionalSeconds]
let advancedDecoder = JSONDecoder()
advancedDecoder.dateDecodingStrategy = .custom { decoder in
let container = try decoder.singleValueContainer()
let str = try container.decode(String.self)
if let date = iso8601Fmt.date(from: str) { return date }
throw DecodingError.dataCorruptedError(in: container,
debugDescription: "Cannot parse date: \(str)")
}The .iso8601 strategy is the right default for most REST APIs. Use .secondsSince1970 for Unix-epoch APIs (common in older backends). Use .custom when you need to try multiple formats or handle timezone offsets beyond the literal Z suffix.
Kotlin Gson: @SerializedName, GsonBuilder, TypeToken, and null safety
Gson is Google's reflection-based JSON library for the JVM. It maps JSON directly onto Kotlin or Java classes without annotation processing. The main pitfall in Kotlin is that Gson bypasses the null-safety system — a non-nullable String field can silently receive null from JSON without throwing. Pair Gson with explicit null checks or switch to Moshi (section 4) for correct Kotlin null-safety enforcement.
// build.gradle.kts
// implementation("com.google.code.gson:gson:2.11.0")
import com.google.gson.Gson
import com.google.gson.GsonBuilder
import com.google.gson.reflect.TypeToken
import com.google.gson.annotations.SerializedName
// ── 1. Basic parsing ──────────────────────────────────────────────────────
data class User(
val id: Int,
val name: String,
val email: String,
@SerializedName("is_active") val isActive: Boolean,
@SerializedName("created_at") val createdAt: String?
)
val gson = Gson()
val json = """{"id":1,"name":"Alice","email":"alice@example.com","is_active":true}"""
val user = gson.fromJson(json, User::class.java)
println(user.name) // Alice
println(user.isActive) // true
// Encode back to JSON
val encoded = gson.toJson(user)
println(encoded) // {"id":1,"name":"Alice","email":"alice@example.com","is_active":true}
// ── 2. Parsing a JSON array with TypeToken ────────────────────────────────
val arrayJson = """[{"id":1,"name":"Alice","email":"a@e.com","is_active":true},
{"id":2,"name":"Bob", "email":"b@e.com","is_active":false}]"""
val listType = object : TypeToken<List<User>>() {}.type
val users: List<User> = gson.fromJson(arrayJson, listType)
println(users.size) // 2
// ── 3. Custom date format with GsonBuilder ────────────────────────────────
val gsonWithDates = GsonBuilder()
.setDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'")
.serializeNulls() // include null fields in output
.setPrettyPrinting()
.create()
// ── 4. Error handling ─────────────────────────────────────────────────────
import com.google.gson.JsonSyntaxException
fun parseUser(json: String): User? {
return try {
gson.fromJson(json, User::class.java)
} catch (e: JsonSyntaxException) {
println("Malformed JSON: ${e.message}")
null
}
}
// ── 5. Nested objects ─────────────────────────────────────────────────────
data class Address(val street: String, val city: String, val zip: String)
data class Customer(val id: Int, val name: String, val address: Address)
val nestedJson = """
{"id":1,"name":"Alice","address":{"street":"123 Main St","city":"Springfield","zip":"12345"}}
"""
val customer = gson.fromJson(nestedJson, Customer::class.java)
println(customer.address.city) // SpringfieldUse TypeToken for any generic type (List<T>, Map<String, T>) because Kotlin/JVM erases generic type parameters at runtime. Without TypeToken, Gson falls back to LinkedTreeMap instead of your data class. See the full guide on parse JSON in Kotlin for Moshi and Retrofit patterns.
Kotlin Moshi: @JsonClass, @Json, adapters, and Retrofit integration
Moshi, maintained by Square, is the modern alternative to Gson for Kotlin. Its key advantage is the @JsonClass(generateAdapter = true) annotation, which triggers code generation at compile time — no reflection, smaller APK, and Kotlin null safety enforced at parse time rather than silently bypassed. A missing required field or a null value for a non-nullable Kotlin property throws JsonDataException immediately.
// build.gradle.kts
// implementation("com.squareup.moshi:moshi:1.15.1")
// implementation("com.squareup.moshi:moshi-kotlin:1.15.1")
// ksp("com.squareup.moshi:moshi-kotlin-codegen:1.15.1") // for @JsonClass
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
import com.squareup.moshi.Moshi
import com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactory
// ── 1. Model with codegen (preferred for Kotlin) ──────────────────────────
@JsonClass(generateAdapter = true)
data class User(
val id: Int,
val name: String,
val email: String,
@Json(name = "is_active") val isActive: Boolean,
@Json(name = "created_at") val createdAt: String? = null
)
// ── 2. Build the Moshi instance ───────────────────────────────────────────
val moshi = Moshi.Builder()
// Add KotlinJsonAdapterFactory only when NOT using codegen (reflection mode)
// .add(KotlinJsonAdapterFactory())
.build()
val adapter = moshi.adapter(User::class.java)
// ── 3. Parse ──────────────────────────────────────────────────────────────
val json = """{"id":1,"name":"Alice","email":"alice@example.com","is_active":true}"""
val user = adapter.fromJson(json) // throws JsonDataException on bad input
println(user?.name) // Alice
// ── 4. Serialize ──────────────────────────────────────────────────────────
val serialized = adapter.toJson(user)
println(serialized)
// {"id":1,"name":"Alice","email":"alice@example.com","is_active":true}
// ── 5. List adapter ───────────────────────────────────────────────────────
import com.squareup.moshi.Types
val listType = Types.newParameterizedType(List::class.java, User::class.java)
val listAdapter = moshi.adapter<List<User>>(listType)
val arrayJson = """[{"id":1,"name":"Alice","email":"a@e.com","is_active":true}]"""
val users = listAdapter.fromJson(arrayJson)
println(users?.size) // 1
// ── 6. Retrofit integration ──────────────────────────────────────────────
// implementation("com.squareup.retrofit2:converter-moshi:2.11.0")
// val retrofit = Retrofit.Builder()
// .baseUrl("https://api.example.com/")
// .addConverterFactory(MoshiConverterFactory.create(moshi))
// .build()
// ── 7. Custom adapter (e.g. for enums or special types) ──────────────────
import com.squareup.moshi.FromJson
import com.squareup.moshi.ToJson
class StatusAdapter {
@FromJson fun fromJson(value: String): Status = Status.valueOf(value.uppercase())
@ToJson fun toJson(status: Status): String = status.name.lowercase()
}
enum class Status { ACTIVE, INACTIVE, PENDING }With @JsonClass(generateAdapter = true) and KSP, Moshi generates a dedicated UserJsonAdapter class at compile time. This adapter is ~3x faster than reflection and correctly propagates Kotlin null-safety errors. If you cannot use KSP, fall back to KotlinJsonAdapterFactory (reflection mode) — slower but still null-safe.
Flutter/Dart: dart:convert, fromJson factory, toJson method, List parsing
Dart's built-in dart:convert library provides jsonDecode and jsonEncode. jsonDecode returns dynamic — typically a Map<String, dynamic> for objects or List<dynamic> for arrays. The standard pattern is a fromJson factory constructor that accepts the decoded map and a toJson method that returns a map for re-encoding. No external packages are required for simple models.
import 'dart:convert';
// ── 1. Model with fromJson / toJson ───────────────────────────────────────
class User {
final int id;
final String name;
final String email;
final bool isActive;
final String? createdAt; // nullable field
const User({
required this.id,
required this.name,
required this.email,
required this.isActive,
this.createdAt,
});
factory User.fromJson(Map<String, dynamic> json) => User(
id: json['id'] as int,
name: json['name'] as String,
email: json['email'] as String,
isActive: json['is_active'] as bool,
createdAt: json['created_at'] as String?,
);
Map<String, dynamic> toJson() => {
'id': id,
'name': name,
'email': email,
'is_active': isActive,
if (createdAt != null) 'created_at': createdAt,
};
}
// ── 2. Parse a single object ───────────────────────────────────────────────
const jsonStr = '{"id":1,"name":"Alice","email":"alice@example.com","is_active":true}';
final user = User.fromJson(jsonDecode(jsonStr) as Map<String, dynamic>);
print(user.name); // Alice
print(user.isActive); // true
// ── 3. Parse a JSON array ─────────────────────────────────────────────────
const arrayStr = '''[
{"id":1,"name":"Alice","email":"a@e.com","is_active":true},
{"id":2,"name":"Bob", "email":"b@e.com","is_active":false}
]''';
final List<dynamic> rawList = jsonDecode(arrayStr) as List<dynamic>;
final users = rawList
.map((e) => User.fromJson(e as Map<String, dynamic>))
.toList();
print(users.length); // 2
// ── 4. Encode to JSON string ───────────────────────────────────────────────
final encoded = jsonEncode(user.toJson());
print(encoded); // {"id":1,"name":"Alice","email":"alice@example.com","is_active":true}
// ── 5. Nested objects ─────────────────────────────────────────────────────
class Address {
final String street;
final String city;
const Address({required this.street, required this.city});
factory Address.fromJson(Map<String, dynamic> j) =>
Address(street: j['street'] as String, city: j['city'] as String);
Map<String, dynamic> toJson() => {'street': street, 'city': city};
}
class Customer {
final int id;
final String name;
final Address address;
const Customer({required this.id, required this.name, required this.address});
factory Customer.fromJson(Map<String, dynamic> j) => Customer(
id: j['id'] as int,
name: j['name'] as String,
address: Address.fromJson(j['address'] as Map<String, dynamic>),
);
}For a complete walkthrough including HTTP client integration with package:http, see parse JSON in Dart/Flutter. The REST API integration section covers decoding the response body from REST API JSON response objects.
Dart json_serializable: @JsonSerializable, build_runner, @JsonKey
For models with many fields, nested objects, or custom key names, the json_serializable package generates fromJson and toJson implementations from annotations — eliminating manual, error-prone boilerplate. Run flutter pub run build_runner build to generate the .g.dart files; the generated code never needs to be edited by hand.
# pubspec.yaml
# dependencies:
# json_annotation: ^4.9.0
# dev_dependencies:
# build_runner: ^2.4.0
# json_serializable: ^6.8.0
import 'package:json_annotation/json_annotation.dart';
part 'user.g.dart'; // generated file
// ── 1. Annotate your class ────────────────────────────────────────────────
@JsonSerializable()
class User {
final int id;
final String name;
final String email;
// Remap snake_case JSON key to camelCase Dart field
@JsonKey(name: 'is_active')
final bool isActive;
// Provide a default value when the key is absent
@JsonKey(name: 'role', defaultValue: 'viewer')
final String role;
// Exclude from serialization
@JsonKey(includeFromJson: false, includeToJson: false)
final String? _cache;
const User({
required this.id,
required this.name,
required this.email,
required this.isActive,
this.role = 'viewer',
String? cache,
}) : _cache = cache;
// Generated factory constructor
factory User.fromJson(Map<String, dynamic> json) => _$UserFromJson(json);
// Generated toJson method
Map<String, dynamic> toJson() => _$UserToJson(this);
}
// ── 2. Nested objects ─────────────────────────────────────────────────────
@JsonSerializable(explicitToJson: true) // <-- required for nested toJson
class Order {
final int id;
final User user; // nested Codable-style object
final List<String> items;
const Order({required this.id, required this.user, required this.items});
factory Order.fromJson(Map<String, dynamic> json) => _$OrderFromJson(json);
Map<String, dynamic> toJson() => _$OrderToJson(this);
}
// ── 3. Custom date conversion ─────────────────────────────────────────────
DateTime _dateFromJson(String v) => DateTime.parse(v);
String _dateToJson(DateTime d) => d.toIso8601String();
@JsonSerializable()
class Event {
final int id;
@JsonKey(fromJson: _dateFromJson, toJson: _dateToJson)
final DateTime startsAt;
const Event({required this.id, required this.startsAt});
factory Event.fromJson(Map<String, dynamic> json) => _$EventFromJson(json);
Map<String, dynamic> toJson() => _$EventToJson(this);
}After changing annotations, re-run the generator: flutter pub run build_runner build --delete-conflicting-outputs. In watch mode during development: flutter pub run build_runner watch. The explicitToJson: true parameter on @JsonSerializable is required whenever a field is itself a serializable class — otherwise toJson serializes nested objects as their toString() representation.
Error handling across platforms: DecodingError, JsonSyntaxException, FormatException
Each platform uses a distinct error type for JSON parse failures. Always catch the platform-specific error type in production code — a generic catch masks the root cause and makes debugging harder. Below are the canonical error-handling patterns for all three platforms.
// ── Swift: DecodingError ─────────────────────────────────────────────────
import Foundation
func parseUser(from data: Data) -> User? {
do {
return try JSONDecoder().decode(User.self, from: data)
} catch let error as DecodingError {
switch error {
case .keyNotFound(let key, let ctx):
print("Missing key '\(key.stringValue)' at \(ctx.codingPath)")
case .typeMismatch(let type, let ctx):
print("Type mismatch: expected \(type) at \(ctx.codingPath)")
case .valueNotFound(let type, let ctx):
print("Null for non-Optional \(type) at \(ctx.codingPath)")
case .dataCorrupted(let ctx):
print("Malformed JSON: \(ctx.debugDescription)")
@unknown default:
print("DecodingError: \(error)")
}
return nil
}
}// ── Kotlin / Gson: JsonSyntaxException + JsonIOException ─────────────────
import com.google.gson.Gson
import com.google.gson.JsonSyntaxException
import com.google.gson.JsonIOException
fun parseUser(json: String): User? {
return try {
Gson().fromJson(json, User::class.java)
} catch (e: JsonSyntaxException) {
// Malformed JSON or type mismatch (e.g. number where string expected)
println("JSON syntax error: ${e.message}")
null
} catch (e: JsonIOException) {
// I/O error while reading the stream
println("JSON I/O error: ${e.message}")
null
}
}
// ── Kotlin / Moshi: JsonDataException + JsonEncodingException ─────────────
import com.squareup.moshi.JsonDataException
import com.squareup.moshi.JsonEncodingException
fun parseMoshiUser(json: String): User? {
val adapter = moshi.adapter(User::class.java)
return try {
adapter.fromJson(json)
} catch (e: JsonDataException) {
// Non-nullable field received null, or required field missing
println("Data error at ${e.message}")
null
} catch (e: JsonEncodingException) {
// Malformed JSON syntax
println("Encoding error: ${e.message}")
null
}
}// ── Dart/Flutter: FormatException ────────────────────────────────────────
import 'dart:convert';
User? parseUser(String json) {
try {
final map = jsonDecode(json);
if (map is! Map<String, dynamic>) {
throw const FormatException('Expected a JSON object');
}
return User.fromJson(map);
} on FormatException catch (e) {
// jsonDecode throws FormatException for malformed JSON
print('Malformed JSON: ${e.message}');
return null;
} catch (e) {
// fromJson can throw TypeError on wrong field types
print('Parse error: $e');
return null;
}
}Swift's codingPath array is particularly useful for debugging nested structures — it pinpoints the exact key sequence that failed (e.g. ["users", "0", "address", "zipCode"]). Moshi's JsonDataExceptionmessage includes the JSON path in a similar format. Dart's FormatException provides an offset into the source string for syntax errors. Always validate JSON in the Jsonic formatter before diagnosing a parse error in your app — many apparent parse bugs are actually malformed server responses.
Definitions
- Codable (Swift)
- A type alias for
Decodable & Encodablein Swift. A type conforming toCodablecan be both decoded from JSON usingJSONDecoderand encoded to JSON usingJSONEncoder. The Swift compiler auto-synthesizes the implementation when all stored properties are themselvesCodable. - CodingKeys
- A nested
enumconforming toCodingKeyinside a SwiftCodabletype. Each case maps a Swift property name to the corresponding JSON key string, enabling fine-grained key remapping without affecting other properties. Properties omitted fromCodingKeysare excluded from both encoding and decoding. - Gson
- A reflection-based JSON library from Google for Java and Kotlin. Gson maps JSON fields to class properties by name (or via
@SerializedNamefor custom mappings). It requires no annotation processing but does not enforce Kotlin null-safety at runtime — non-nullable Kotlin fields can silently receivenull. - Moshi
- A modern JSON library from Square for Kotlin and Java. Moshi supports compile-time code generation via
@JsonClass(generateAdapter = true), which produces type-safe adapters that correctly enforce Kotlin null-safety. Missing required fields and null values for non-nullable fields throwJsonDataExceptionat parse time. - dart:convert
- The Dart SDK's built-in library for encoding and decoding data. For JSON, it provides
jsonDecode(String)(returnsdynamic) andjsonEncode(Object)(returnsString). No external dependency is required. The decoded value is typically aMap<String, dynamic>that must be manually cast and mapped to a model class. - json_serializable
- A Dart/Flutter code-generation package that produces
fromJsonandtoJsonimplementations from@JsonSerializableand@JsonKeyannotations. Run viabuild_runner; generates.g.dartfiles that implement the boilerplate automatically. - dateDecodingStrategy
- A property on Swift's
JSONDecoderthat controls how JSON date values are converted to SwiftDateobjects. Available strategies include.iso8601,.secondsSince1970,.millisecondsSince1970,.formatted(DateFormatter), and.customfor arbitrary parsing logic.
Frequently asked questions
How do I parse JSON in Swift?
Conform your struct or class to Codable (or Decodable if you only need to read), then call JSONDecoder().decode(YourType.self, from: data). The data parameter is Data — convert a JSON string with string.data(using: .utf8)!. JSONDecoder throws a DecodingError on failure, so wrap it in a do { try ... } catch block. Property names must match JSON keys exactly by default; use CodingKeys or keyDecodingStrategy = .convertFromSnakeCase to remap them. See the full guide on parse JSON in Swift for comprehensive examples.
How do I handle snake_case JSON keys in Swift Codable?
Set decoder.keyDecodingStrategy = .convertFromSnakeCase for automatic bulk conversion — this maps user_id to userId, created_at to createdAt, and so on. For per-field control, add a nested enum CodingKeys: String, CodingKeyinside your struct and set each case's raw value to the exact JSON key string. Both approaches can coexist: convertFromSnakeCase applies to any key not explicitly overridden by CodingKeys.
How do I parse JSON in Kotlin with Gson?
Add com.google.code.gson:gson to your dependencies, define a Kotlin data class, and call Gson().fromJson(jsonString, YourClass::class.java). Annotate fields that differ from JSON keys with @SerializedName("field_name"). For generic types like List<User>, use object : TypeToken<List<User>>(){}.type to preserve the generic type parameter at runtime. Wrap calls in a try/catch for JsonSyntaxException.
What is the difference between Gson and Moshi in Android?
Gson is reflection-based and requires no build setup, but it silently ignores Kotlin null-safety — a String field (non-nullable) can receive null without throwing. Moshi with @JsonClass(generateAdapter = true) generates type-safe adapters at compile time, correctly enforces Kotlin null-safety (throwing JsonDataException for violations), and produces smaller APKs when optimized with R8. For new Kotlin projects, Moshi is the recommended choice.
How do I parse JSON in Flutter with dart:convert?
Import dart:convert, call jsonDecode(response.body) as Map<String, dynamic>, then pass the map to a fromJson factory constructor on your model class. Each field is manually extracted: json['id'] as int. For encoding, call jsonEncode(model.toJson()) where toJson returns a Map<String, dynamic>. See the guide on parse JSON in Dart/Flutter for full HTTP client integration.
How do I handle null values in JSON parsing on iOS and Android?
In Swift, declare the property as T? — JSONDecoder sets it to nil when the JSON value is null or the key is absent. In Kotlin with Gson, declare the field as nullable (String?) — Gson sets it to null, but non-nullable fields can also receive null silently. With Moshi, a non-nullable field that receives null or a missing key throws JsonDataException immediately. In Dart, use json['key'] as String? or provide a fallback: (json['key'] as String?) ?? 'default'.
How do I parse JSON date strings in Swift and Kotlin?
In Swift, set decoder.dateDecodingStrategy before decoding: use .iso8601 for "2026-05-19T17:00:00Z", .secondsSince1970 for Unix timestamps, or .formatted(dateFormatter) for custom formats. In Kotlin with Gson, use GsonBuilder().setDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'").create() and declare the field as java.util.Date. With Moshi, use a custom JsonAdapter<Date> or the moshi-adapters artifact which includes RFC 3339 date support. For a cross-platform comparison, see the JSON date formats guide.
Should I use Gson or Moshi for Android JSON parsing?
Use Moshi for new Kotlin Android projects. Moshi's codegen produces type-safe adapters that correctly enforce Kotlin null-safety, generate no reflection overhead, and produce smaller APKs with R8. Gson is a fine choice for existing Java codebases or when annotation processing is undesirable — it requires zero configuration for simple classes. Both libraries have Retrofit converters (converter-gson and converter-moshi). Gson has not had a major feature release in years; Moshi is actively maintained and better aligned with modern Kotlin idioms.
Validate your JSON before parsing in your app
Paste your API response into the Jsonic formatter to catch syntax errors, null mismatches, and key naming issues before they surface as runtime crashes on iOS, Android, or Flutter.
Open JSON FormatterFurther reading and primary sources
- Swift Codable — Apple Developer Documentation — Official reference for the Codable type alias and its synthesis rules
- Gson User Guide — Google — Complete guide to Gson serialization, custom adapters, and TypeToken
- Moshi — Square — Moshi documentation: codegen, adapters, and Retrofit integration
- dart:convert library — Dart SDK — Official Dart documentation for jsonDecode, jsonEncode, and converters
- json_serializable — pub.dev — Flutter/Dart code generation package for fromJson and toJson