iOS / Swift / Concurrency and data boundaries / Lesson 8
URLSession, Codable, and response contracts
Separate transport success, HTTP acceptance, decoding, and domain validation before publishing remote data.
On this page
Lesson goals
- One request crosses several contracts
- Decode a transport model with explicit types
- Check the HTTP boundary before the success body
One request crosses several contracts
Completing a URLSession data task means the transport produced a response or error; it does not mean an HTTP operation was accepted or its body matches your model. Validate the expected response type and permitted status codes before decoding the success payload. A server can return a valid JSON error body with an unsuccessful status. Likewise, status 200 can carry malformed or semantically invalid data. Keep transport, protocol, decoding, and domain failure categories distinct enough for recovery. Do not present all four as an empty list, because users cannot distinguish no results from a failed operation.
Decode a transport model with explicit types
Codable can synthesize mapping for suitable properties, but it does not decide business invariants such as a nonempty identifier or nonnegative seats. The first fragment decodes a small synthetic fixture, then checks those conditions before presentation. Missing required fields and wrong types cause decoding failures, while additional fields are usually ignored by a synthesized decoder unless you choose stricter behavior. Decide your compatibility policy deliberately. Keep DTOs separate from richer domain models when normalization or cross-record consistency is required, rather than assuming successful decoding establishes every application rule.
import Foundation
struct WorkshopDTO: Decodable {
let id: String
let seats: Int
}
enum DecodeFailure: Error { case invalidWorkshop }
func decodeWorkshop(_ data: Data) throws -> WorkshopDTO {
let item = try JSONDecoder().decode(WorkshopDTO.self, from: data)
guard !item.id.isEmpty, item.seats >= 0 else { throw DecodeFailure.invalidWorkshop }
return item
}
let fixture = Data(#"{"id":"w-7","seats":3}"#.utf8)
// try decodeWorkshop(fixture).seats -> 3
The accepted fixture has identifier w-7 and seats 3.
This only decodes local bytes. It performs no network request and does not enforce authoritative capacity.
Check the HTTP boundary before the success body
The second fragment shows an illustrative loader receiving a URL from a reviewed caller. It accepts only a narrow successful status and then uses the preceding decoder. Production contracts may permit several statuses or no-content responses; define those explicitly instead of blindly broadening a range. The session is injected to make transport substitution possible. No endpoint is supplied or contacted here. A real application must also define timeout, cancellation, authentication, privacy, and allowed-origin policy at its appropriate boundary. Never concatenate untrusted input into an arbitrary URL and treat it as an authorized destination.
enum LoadFailure: Error { case unexpectedResponse }
func loadWorkshop(from url: URL, session: URLSession) async throws -> WorkshopDTO {
let (data, response) = try await session.data(from: url)
guard let http = response as? HTTPURLResponse, http.statusCode == 200 else {
throw LoadFailure.unexpectedResponse
}
return try decodeWorkshop(data)
}
Only an HTTP 200 response with a valid workshop body becomes a model.
Foundation and the earlier decoder are required. This network fragment is unexecuted and has no configured endpoint.
Preserve error context without exposing payloads
A DecodingError can identify a coding path and expected shape, which helps locate a schema disagreement. Record safe field context and the selected model version rather than logging complete responses that may contain personal data. Translate failure categories into useful user messages near the presentation layer. Retrying malformed bytes repeatedly is unlikely to fix an incompatible schema, while a transient transport failure may permit a bounded retry under the service policy. For writes, distinguish rejected commands from uncertain completion so retry does not accidentally duplicate a successful server operation.
Keep loading state independent from cached content
A refresh can be pending while previous valid data remains useful. Model that state explicitly instead of erasing the screen every time a request begins. When an error occurs, identify whether it belongs to initial loading or refreshing existing content. Apply request identity checks before committing either success or failure, since a typed decoded value can still belong to an obsolete query or account. Decoding establishes a valueโs shape, not its freshness. Scope caches by identity and ownership, and define whether unavailable records invalidate an existing selection.
Verify fixtures and response classes independently
Test local fixtures for valid data, missing fields, wrong types, negative seats, and extra fields under the chosen compatibility rule. Test a fake HTTP boundary separately for success, error status with JSON, non-HTTP response, and malformed success data. Do not call a real service merely to validate the decoder. Actual URLSession transport, certificate handling, background sessions, and device network transitions require their own authorized integration checks. No such request or SDK execution took place in the course authoring environment; the code blocks describe expected contracts only.
Common errors and fixes
A JSON error response appears as successful content
- Why it happens
- The code decoded before evaluating HTTP acceptance.
- How to fix it
- Validate response type and status under the endpoint contract.
- Prevent it next time
- Include valid JSON with an unsuccessful status in fixtures.
Decoded negative seats reach the UI
- Why it happens
- Codable shape validation was mistaken for domain validation.
- How to fix it
- Check model invariants after or within decoding.
- Prevent it next time
- Test semantically invalid but syntactically valid JSON.
Diagnostics expose full response data
- Why it happens
- Logging collected more content than the debugging question required.
- How to fix it
- Record sanitized field paths and failure categories.
- Prevent it next time
- Review logging with synthetic sensitive fixtures.
Check your understanding
1. Why does a successful URLSession await not establish a valid workshop?
Show worked solution
The response may have an error status, wrong content shape, or invalid business values. Check HTTP acceptance, decode the expected model, and validate invariants before publication. Then still check request freshness and authorization for consequential use.
2. Should seats encoded as the string "3" be silently converted?
Show worked solution
Only if the documented service contract permits that representation. Otherwise reject the mismatch so schema defects are visible. If conversion is supported, centralize it at the decoding boundary with explicit range and ambiguity tests rather than parsing opportunistically in views.
Key takeaways
- One request crosses several contracts
- Verify fixtures and response classes independently