southkin/haslazyserver
> Coping tools for gaslighting JSON.
π§© The Problem: βJust Pick a Type. Please.β
Sometimes it's a number. Sometimes it's a string. Sometimes it's null. Sometimes it's a whole object pretending to be a number. And the API doc? It swears it's always an Int.
π§ The Solution
You can't change the backend. But you can protect your decoder β and your sanity.
HasLazyServer provides Codable types that accept the chaos, convert it safely, and log when things go sideways (in DEBUG).
β MaybeString & MaybeNumber
These types help you decode values that shift between string, int, or double. They normalize the result to a usable format, and log inconsistencies.
struct Item: Codable {
let price: MaybeNumber
let title: MaybeString
}
let jsonList = [
#"{"price": 100.0, "title": "Book"}"#,
#"{"price": "100.0", "title": 3001}"#,
]
jsonList.forEach {
guard let item:Item = $0.data?.makeObj() else {return}
print("π° Price as Double:", item.price.asDouble)
print("π° Price as Int:", item.price.asInt)
print("πͺͺ Title:", item.title.asString)
}result
π° Price as Double: Optional(100.0)
π° Price as Int: Optional(100)
πͺͺ Title: Optional("Book")
β οΈ [MaybeNumber] 'price' got String instead of Int/Double
β οΈ [MaybeString] 'title' got Int instead of String
π° Price as Double: Optional(100.0)
π° Price as Int: Optional(100)
πͺͺ Title: Optional("3001")β SometimeArray
Sometimes you get one, sometimes many. SometimeArray remembers which it was β but gives you .asArray when you just want to move on.
struct User: Codable {
let name: String
let email: String?
let age: MaybeNumber?
}
struct UserList: Codable {
let users: SometimeArray<User>
}
let jsonList = [
#"{"name": "john", "email": "john@example.com", "age": "30"}"#,
#"[{"name": "john1", "email": "john1@example.com", "age": 30},{"name": "john2", "email": "john2@example.com", "age": "30"}]"#
]
jsonList.forEach {
guard let userList:UserList = $0.data?.makeObj() else { return }
userList.users.asArray.forEach {
print($0.name)
print($0.email)
print($0.age)
}
}result
β οΈ [MaybeNumber] 'users.age' got String instead of Int/Double
john
Optional("john@example.com")
Optional(30)
β οΈ [MaybeNumber] 'users.Index 1.age' got String instead of Int/Double
john1
Optional("john1@example.com")
Optional(30)
john2
Optional("john2@example.com")
Optional(30)π Debug Output
In DEBUG builds, HasLazyServer prints logs for type mismatches. So you know when and where your trust was betrayed.
β οΈ [MaybeNumber] 'price' got String instead of Int/Double
β οΈ [MaybeString] 'title' got Int instead of Stringπ§ͺ Full Example with API Layer
These types work great standalone β but also integrate cleanly with FullyRESTful, a declarative Swift API library for REST and WebSocket.
import FullyRESTful
struct Book : Codable {
let name:MaybeString
let author:MaybeString
let price:MaybeNumber
let code:MaybeString?
}
struct Search : APIITEM {
struct Request : Codable {
let searchText:String
}
struct Response : Codable {
let items:SometimeArray<Book>
let totalCount:MaybeNumber
}
let requestModel = Request.self
let responseModel = Response.self
var method:HTTPMethod = .GET
var customHeader: [String : String]?
let server: ServerInfo = .init(domain: "https://myServer.com:8080", defaultHeader: ["Content-Type": "application/json; utf-8"])
let path = "/library/books/search"
}
Task {
let bookList = try? await Search().request(param: .init(searchText: "API Docs"))?.model?.items.asArray
bookList?.forEach {
print($0.name.asString)
print($0.author.asString)
}
}π Bonus: InsistsNonNull
When something is supposed to never be null, but reality disagrees β this type lets you keep going, while leaving a paper trail in your logs.
let price:InsistsNonNull<MaybeNumber>π§ Closing Thoughts
Your app deserves to live. You deserve to sleep.
Let HasLazyServer handle the weird stuff. And protect your mental health β it's the only real production system you run.
π License
MIT License
Package Metadata
Repository: southkin/haslazyserver
Default branch: main
README: README.md