I have this API response structure (from Strapi v4):
{
"data": [
{
"id": 1,
"attributes": {
"description": "test",
}
}
]
}
I have this generic code to handle API responses and to inject the ID to my child object:
struct StrapiArrayResponse<Content: StrapiDataObjectContent>: Codable {
var data: [StrapiDataObject<Content>]
}
struct StrapiDataObject<Content: StrapiDataObjectContent>: Codable {
let id: Int
var attributes: Content
init(from decoder: Decoder) throws {
let container: KeyedDecodingContainer<CodingKeys> = try decoder.container(keyedBy: CodingKeys.self)
self.id = try container.decode(Int.self, forKey: .id)
self.attributes = try container.decode(Content.self, forKey: .attributes)
self.attributes.id = id
}
}
protocol StrapiDataObjectContent: Codable {
var id: Int! { get set } // I don't want this to be an Optional
}
I want my id to be a let
instead of an optional var
.
Is there a better way to inject the ID to my child objects (StrapiDataObjectContent
)?
Here is a solution for the problem but it isn't so straightforward and requires some work.
Since you want id
to be a constant we need a way to initialise Content
with it so one way then is to add an init
to the protocol.
protocol StrapiDataObjectContent: Codable {
var id: Int { get } //also removed 'set'
init(id: Int, copy: Self)
}
As you see this init takes an already existing object as parameter so this is kind of a copy method
So an implementation (based on the json in the question) could then be
init(id: Int, copy: Test) {
self.id = id
self.description = copy.description
}
We then need to change init(from:)
in StrapiDataObject
to
init(from decoder: Decoder) throws {
let container: KeyedDecodingContainer<CodingKeys> = try decoder.container(keyedBy: CodingKeys.self)
self.id = try container.decode(Int.self, forKey: .id)
let attributes = try container.decode(Content.self, forKey: .attributes)
self.attributes = Content(id: id, copy: attributes)
}
Now this compiles but we will get a runtime error since id
is expected by the decoder for Content
but doesn't exists in the json.
So this leads to the major drawback of this solution, every type conforming to StrapiDataObjectContent
needs to implement a custom init(from:)
just to avoid decoding the id
property
To demonstrate here is a full example (based on the json in the question)
struct Test: StrapiDataObjectContent {
let id: Int
let description: String
init(from decoder: Decoder) throws {
id = 0
let container = try decoder.container(keyedBy: CodingKeys.self)
description = try container.decode(String.self, forKey: .description)
}
init(id: Int, copy: Test) {
self.id = id
self.description = copy.description
}
}