I am struggling to create a swift struct, where I can use a property wrapper to decode some JSON that sometimes requires a value to conform to a specific type, but other times may become a dictionary with a store ID reference.
Here is an example struct:
struct JobDetail: Codable {
var title: String
var years: Int
}
struct Person: Codable {
var name: String
var age: Int
var job: JobDetail
}
struct Employee: Codable {
@UUIDEmbedded var info: Person
}
For example, in the Employee
struct above, I can assign a unique ID to the info variable - I can also decide not to, in which case, the ID would just default to nil. In JSON form, it could look like either of the following...
{
"info": {"id": "068D61D7-9BA9-4459-B328-1FF801F52264", "value": {"name": "John", "age": 30, "job": {"title": "CEO", "years": 5}}}
}
or
{
"info": {"name": "John", "age": 30, "job": {"title": "CEO", "years": 5}}
}
While the code I currently have works great for this kind of thing and can automatically distinguish a "info" variable with an ID, from one without an ID, the problem occurs when I try to then use this property wrapper on anything other than a custom struct. For example, if I want to give the "age" variable in Person
the @UUIDEmbedded
property wrapper.
struct Person: Codable {
var name: String //works with UUIDEmbedded: ❌
@UUIDEmbedded var age: Int //works with UUIDEmbedded: ❌
var job: JobDetail //works with UUIDEmbedded: ✅
}
⚠️ Error: typeMismatch(Swift.Dictionary<Swift.String, Any>, Swift.DecodingError.Context(codingPath: [CodingKeys(stringValue: "info", intValue: nil), CodingKeys(stringValue: "value", intValue: nil), CodingKeys(stringValue: "age", intValue: nil)], debugDescription: "Expected to decode Dictionary<String, Any> but found number instead.", underlyingError: nil))
❌ Doesn't work for JSON:
{
"info": {"name": "John", "age": 30, "job": {"title": "CEO", "years": 5}}
}
✅ Does work for JSON:
{
"info": {"name": "John", "age": {"id": "884D61D7-9BA9-4459-B328-1FF801F52264", "value": 30}, "job": {"title": "CEO", "years": 5}}
}
It should be working for both.
Thank you for all of you help in advance! 😊
Property Wrapper
@propertyWrapper
struct UUIDEmbedded<T: Codable>: Codable {
private var value: T
private var id: UUID?
var wrappedValue: T {
get { value }
set { value = newValue }
}
var projectedValue: UUID? {
get { id }
set { id = newValue }
}
init(wrappedValue: T) {
self.value = wrappedValue
}
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
if container.contains(.id) {
if let idString = try? container.decode(String.self, forKey: .id) {
self.id = UUID(uuidString: idString)
}
}
if container.contains(.value) {
self.value = try container.decode(T.self, forKey: .value)
} else {
// If value key is not found, decode the entire container as the value
self.value = try T(from: decoder)
}
}
func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
if let id = id {
try container.encode(id, forKey: .id)
}
try container.encode(value, forKey: .value)
}
private enum CodingKeys: String, CodingKey {
case id
case value
}
}
Decoding
let jsonStringWithoutID = """
{
"info": {"name": "John", "age": 30, "job": {"title": "CEO", "years": 5}}
}
"""
do {
let decoder = JSONDecoder()
// Decoding JSON without ID
let decodedStructWithoutID = try decoder.decode(Employee.self, from: Data(jsonStringWithoutID.utf8))
print(decodedStructWithoutID.info.name) // Output: John
if let uuid = decodedStructWithoutID.$info {
print(uuid) // Output: nil
} else {
print("UUID is nil")
}
print(decodedStructWithoutID.info.age)
print(decodedStructWithoutID.info.job.title)
} catch {
print(error)
}
Use decoder.singleValueContainer()
to determine if the value is a single fundamental type (String, Int, Boolean, etc.). Otherwise, catch custom structs and or @UUIDEmbedded
key-value pairs using the container<CodingKey>(keyedBy type: CodingKey.Type)
decoder method.
Here is the code that needs to be implemented...
do {
let container = try decoder.singleValueContainer()
self.value = try container.decode(T.self)
} catch {
//Original code here
}
Altogether, the decoder for the property wrapper would look like this...
init(from decoder: Decoder) throws {
do {
let container = try decoder.singleValueContainer()
self.value = try container.decode(T.self)
} catch {
let container = try decoder.container(keyedBy: CodingKeys.self)
if container.contains(.id) {
if let idString = try? container.decode(String.self, forKey: .id) {
self.id = UUID(uuidString: idString)
}
}
if container.contains(.value) {
self.value = try container.decode(T.self, forKey: .value)
} else {
// If value key is not found, decode the entire container as the value
self.value = try T(from: decoder)
}
}
}