I have a Codable object that I need to convert to Dictionary so I first encode it to convert it to Data and then use JSONSerialization to convert that Data to Any and then use as? [String: Any] to get the dictionary.
The conversion is successful but due to use of JSONSerialisation, Bool types are being converted to NSNumber but I want to retain the Bool value inside the Dictionary
import Foundation
struct Employee: Codable {
let employeeID: Int?
let meta: Meta?
}
struct Meta: Codable {
let demo: Bool?
}
let employeeObject = Employee(employeeID: 1, meta: Meta(demo: true))
do {
let encodedObject = try JSONEncoder().encode(employeeObject)
let dictionary = try JSONSerialization.jsonObject(with: encodedObject, options: .fragmentsAllowed) as? [String: Any]
print(dictionary ?? [:])
} catch {
print(error)
}
OUTPUT
["meta": {
demo = 1; }, "employeeID": 1]
demo property is being converted to NSNumber but I want to retain the Bool value
The print output is misleading. That's related to internal implicit bridging to NSDictionary
Although the value is printed as 1 the type is a boolean, see
let meta = dictionary!["meta"] as! [String:Any]
print(type(of: meta["demo"]! )) // __NSCFBoolean
Your concern is causeless because to get the value from a [String:Any] dictionary you have to cast the type anyway
let demo = meta[demo] as! Bool
Or you have to cast the dictionary to [String:Bool]
let meta = dictionary!["meta"] as! [String:Bool]
let demo = meta["demo"]!
Side note:
Declare struct members non-optional as much as possible, not the opposite.
This should do the trick:
struct Meta: Codable {
let demo: Bool?
public init(demo : Bool) {
self.demo = demo
}
enum CodingKeys: String, CodingKey {
case demo = "demo"
}
public init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
demo = try container.decode(Bool.self, forKey: .demo)
}
public func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(self.demo, forKey: .demo)
}
}
The JSON representation is still a number, but it decodes correctly back into Bool.
Related
I have a custom struct I want to store in AppStorage:
struct Budget: Codable, RawRepresentable {
enum CodingKeys: String, CodingKey {
case total, spent
}
var total: Double
var spent: Double
init(total: Double = 5000.0, spent: Double = 3000.0) {
self.total = total
self.spent = spent
}
init?(rawValue: String) {
guard let data = rawValue.data(using: .utf8),
let result = try? JSONDecoder().decode(Budget.self, from: data)
else { return nil }
self = result
}
var rawValue: String {
guard let data = try? JSONEncoder().encode(self),
let result = String(data: data, encoding: .utf8)
else {
return ""
}
return result
}
}
I then have the following view:
struct DemoView: View {
#AppStorage(UserDefaults.StorageKeys.budget.rawValue) var budget = Budget()
var body: some View {
Button("Update") {
budget.total = 10
}
}
}
When I tap the button the app crashes with Thread 1: EXC_BAD_ACCESS on guard let data = try? JSONEncoder().encode(self) for rawValue in Budget. What am I doing wrong here?
You are running into infinite recursion. This is because types that conforms to both Encodable and RawRepresentable automatically get this encode(to:) implementation (source), which encodes the raw value. This means that when you call JSONEncoder().encode, it would try to call the getter of rawValue, which calls JSONEncoder().encode, forming infinite recursion.
To solve this, you can implement encode(to:) explicitly:
func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(total, forKey: .total)
try container.encode(spent, forKey: .spent)
}
Note that you should also implement init(from:) explicitly, because you also get a init(from:) implementation (source) that tries to decode your JSON as a single JSON string, which you certainly do not want.
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
total = try container.decode(Double.self, forKey: .total)
spent = try container.decode(Double.self, forKey: .spent)
}
This question already has answers here:
Using codable with value that is sometimes an Int and other times a String
(5 answers)
Closed last year.
i am working with a service that have both a websocket for live data and a api for historical data
the JSON looks similar and i would like to decode it to the same object
the only difference is that in the live one variable is a number but as a string and with the historical data the number is an int.
and preferably i would like to not have to create 2 almost identical decodable objects.
have anyone tried something similar.
You have to define a single type (Int or String) for your data structure and use init with Decoder to make a custom parsing.
struct MyData: Decodable {
let value: Int // Could be Int or String from different services
}
extension MyData {
enum CodingKeys: String, CodingKey {
case value
}
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
do {
value = try container.decode(Int.self, forKey: .value)
} catch {
let stringValue = try container.decode(String.self, forKey: .value)
if let valueInt = Int(stringValue) {
value = valueInt
} else {
var codingPath = container.codingPath
codingPath.append(CodingKeys.value)
let debugDescription = "Could not create Int from String \(stringValue) of field \(CodingKeys.value.rawValue)"
let context = DecodingError.Context(codingPath: codingPath, debugDescription: debugDescription)
throw DecodingError.dataCorrupted(context)
}
}
}
}
I think you need a wrapper for that case of some sort. To make it as convenient as possible you could use a property wrapper for this
#propertyWrapper
struct NormalOrStringyInt: Decodable {
var wrappedValue: Int?
init(from decoder: Decoder) throws {
let container = try decoder.singleValueContainer()
if let value = try? container.decode(Int.self) {
wrappedValue = value
} else if
let string = try? container.decode(String.self),
let value = Int(string)
{
wrappedValue = value
} else {
wrappedValue = nil // default value
}
}
}
struct Model: Codable {
#NormalOrStringyInt var id: Int?
var someInt: Int
var someString: String
...
}
let model = try! JSONDecoder().decode(Model, from: data)
let id: Int? = model.id.wrappedValue
I'm trying to parse a json that looks like this using decodable:
{
"count": 1,
"results": [
{
"title": 1,
"content": "Bla"
} ]
}
My problem is that I don't want to make a class that has a count property just to be able to use the decoder. I want to parse only the results part I don't care about the count.
So my question is, can decodable.decode somehow only parse a part of the result json. I mean a certain key path instead of the whole json ? And I want to do it using Decodable.
In a nutshell I don't want this:
class IncidentWrapper: Codable{
var count: Int
var incident: [Incident]
}
What I would Imagine is to have this:
decodable.decode([Incident].self, from: response.data, forKey: "results")
Thanks
let me see what I can suggest:
struct Result: Codeable {
var id: Int
var message: String
var color: String
var type: String
enum CodingKeys: String, CodingKey {
case results
}
enum NestedResultKeys: String, CodingKey {
case id, message, color, type
}
}
extension Result: Decodable {
init(from decoder: Decoder) throws {
let result = try decoder.container(keyedBy: CodingKeys.self)
let nestedResult = try result.nestedContainer(keyedBy: NestedResultKeys.self, forKey: .result)
id = try nestedResult.decode(Int.self, forKey: .id)
message = try nestedResult.decode(String.self, forKey: .message)
color = try nestedResult.decode(String.self, forKey: .color)
type = try nestedResult.decode(String.self, forKey: .id)
}
}
See this documentation for more insight
https://developer.apple.com/documentation/swift/swift_standard_library/encoding_decoding_and_serialization
Hope it helps your project!
You probably is looking for JSONSerialization class. This is an example how it works:
if let json = try? JSONSerialization.jsonObject(with: data, options: []) as? [String : Any] {
if let results = json["results"] as? [Incident] {
print(results.count)
}
}
You can define a generic wrapper for once and use everywhere.
It will work as a generic wrapper for results key only.
protocol ResultsDecodable: Decodable{
associatedtype T: Decodable
var results: [T] {get}
}
struct Result<Element: Decodable>: ResultsDecodable{
typealias T = Element
var results: [Element]
}
Extend JSONDecoder to get results output.
extension JSONDecoder {
func resultDecode<T>(_ type: Result<T>.Type, from data: Data) throws -> [T] where T : Decodable{
let model = try! decode(type, from: data)
return model.results
}
}
And use like this
var str = #"{"count": 1,"results": [{"title": 1,"content": "Bla"}, {"title": 2,"content": "Bla"} ]}"#
class Incident: Decodable{
let title: Int
let content: String
}
let indicents = (try! JSONDecoder().resultDecode(Result<Incident>.self, from: str.data(using: .utf8)!))
See how it makes everything more complex. BETTER USE IncidentWrapper!!!
You only need to use the keys you care about. Just leave off the count. Don't make it part of your struct.
You will only get errors if you can't find a key in the json that you are expecting in the struct. You can avoid this too, though, if you make it an optional in the struct.
I have the following object
struct Properties: Decodable {
var id: String?
var value: String?
var color: String?
}
In the first request to server I get the following response
{
"id":"1",
"color":"red"
}
And after another request I get
{
"id":"1", // the id of the object props is meant for
"props":{
"value":"my value" // I can get any property here
}
}
After the two requests I should have the object with all properties set
By now I decode the second request as following
struct SetAttr: Decodable {
let id: String
let props: [String : Any]
enum SetAttrCodingKeys: String, CodingKey {
case id
case props
}
init(from decoder: Decoder) throws {
let container = try! decoder.container(keyedBy: SetAttrCodingKeys.self)
props = try! container.decode([String : Any].self, forKey: .props)
id = try! container.decode(String.self, forKey: .id)
}
}
But I do not know how to parse props dictionary and set the properties on the first object. I am willing to use a decoding library, but I did not find any that can do this
EDIT:
This is how I tried to set the properties from dictionary, but the solution is not scalable
var myObject: Properties
properties = setAttr.props // [String:Any]
let keys = properties.keys
keys.forEach { key in
if let value = properties[key] {
switch key {
case "value":
myObject.value = value as? String
case "color":
myObject.color = value as? String
default:
break
}
}
}
Just use JSONSerialization which parses whatever you throw at it into arrays and dictionaries. That frees you from all the problems you have with strangely formatted JSON.
For example, the second request will be parsed as a dictionary with two keys "id" and "props", and "props" has a value which is again a dictionary with one key "value" and a value "my value".
And please stop using try! That will cause your app to crash instantly if any input is not expected. Unexpected inputs should be handled, not lead to a crash.
There are various way to do this, but one possible way could be something like this:
struct SecAttr: Decodable {
let id: String
var props: Properties?
private enum CodingKeys: String, CodingKey {
case id
case props
}
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
id = try container.decode(String.self, forKey: .id)
if let props = try container.decodeIfPresent(Properties.self, forKey: .props) {
self.props = props
} else {
// decode Properties from the same object
self.props = try Properties(from: decoder)
}
}
}
struct Properties: Decodable {
var value: String?
var color: String?
mutable update(from props: Properties) {
value = props.value ?? value
color = color.value ?? color
}
}
Now you can decode your original object and after getting updated properties, just update them on the original.
I'm using the Decodable protocol in order to parse JSON received from an external source. After decoding the attributes that I do know about there still may be some attributes in the JSON that are unknown and have not yet been decoded. For example, if the external source added a new attribute to the JSON at some future point in time I would like to hold onto these unknown attributes by storing them in a [String: Any] dictionary (or an alternative) so the values do not get ignored.
The issue is that after decoding the attributes that I do know about there isn't any accessors on the container to retrieve the attributes that have not yet been decoded. I'm aware of the decoder.unkeyedContainer() which I could use to iterate over each value however this would not work in my case because in order for that to work you need to know what value type you're iterating over but the value types in the JSON are not always identical.
Here is an example in playground for what I'm trying to achieve:
// Playground
import Foundation
let jsonData = """
{
"name": "Foo",
"age": 21
}
""".data(using: .utf8)!
struct Person: Decodable {
enum CodingKeys: CodingKey {
case name
}
let name: String
let unknownAttributes: [String: Any]
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
self.name = try container.decode(String.self, forKey: .name)
// I would like to store the `age` attribute in this dictionary
// but it would not be known at the time this code was written.
self.unknownAttributes = [:]
}
}
let decoder = JSONDecoder()
let person = try! decoder.decode(Person.self, from: jsonData)
// The `person.unknownAttributes` dictionary should
// contain the "age" attribute with a value of 21.
I would like for the unknownAttributes dictionary to store the age attribute and value in this case and any other possible value types if they get added to the JSON from the external source in the future.
The reason I am wanting to do something like this is so that I can persist the unknown attributes present in the JSON so that in a future update of the code I will be able to handle them appropriately once the attribute keys are known.
I've done plenty of searching on StackOverflow and Google but haven't yet encountered this unique case. Thanks in advance!
You guys keep coming up with novel ways to stress the Swift 4 coding APIs... ;)
A general solution, supporting all value types, might not be possible. But, for primitive types, you can try this:
Create a simple CodingKey type with string-based keys:
struct UnknownCodingKey: CodingKey {
init?(stringValue: String) { self.stringValue = stringValue }
let stringValue: String
init?(intValue: Int) { return nil }
var intValue: Int? { return nil }
}
Then write a general decoding function using the standard KeyedDecodingContainer keyed by the UnknownCodingKey above:
func decodeUnknownKeys(from decoder: Decoder, with knownKeys: Set<String>) throws -> [String: Any] {
let container = try decoder.container(keyedBy: UnknownCodingKey.self)
var unknownKeyValues = [String: Any]()
for key in container.allKeys {
guard !knownKeys.contains(key.stringValue) else { continue }
func decodeUnknownValue<T: Decodable>(_ type: T.Type) -> Bool {
guard let value = try? container.decode(type, forKey: key) else {
return false
}
unknownKeyValues[key.stringValue] = value
return true
}
if decodeUnknownValue(String.self) { continue }
if decodeUnknownValue(Int.self) { continue }
if decodeUnknownValue(Double.self) { continue }
// ...
}
return unknownKeyValues
}
Finally, use the decodeUnknownKeys function to fill your unknownAttributes dictionary:
struct Person: Decodable {
enum CodingKeys: CodingKey {
case name
}
let name: String
let unknownAttributes: [String: Any]
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
self.name = try container.decode(String.self, forKey: .name)
let knownKeys = Set(container.allKeys.map { $0.stringValue })
self.unknownAttributes = try decodeUnknownKeys(from: decoder, with: knownKeys)
}
}
A simple test:
let jsonData = """
{
"name": "Foo",
"age": 21,
"token": "ABC",
"rate": 1.234
}
""".data(using: .utf8)!
let decoder = JSONDecoder()
let person = try! decoder.decode(Person.self, from: jsonData)
print(person.name)
print(person.unknownAttributes)
prints:
Foo
["age": 21, "token": "ABC", "rate": 1.234]