Parsing JSON with Argo on Swift - json

I'm attempting to parse JSON data with next format:
["1", "string", "", "0.16"]
And these "weird" json should map in next way to my object:
myObject.id = json[0] //"1"
myObject.name = json[1] //"string"
myObject.surname = json[2] // ""
myObject.length = json[3] // "0.16"
I'm using Argo for parsing, there is example of my code
public struct SomeObject {
public var id: String
public var name: String
public var surname: String
public var length: Float
}
extension SomeObject: Decodable {
static func create(id: String)(name: String)(surname: String)(length: String) -> SomeObject {
return SomeObject(id: id, name: name, surname: surname, length: length)
}
public static func decode(json: JSON) -> Decoded<SomeObject> {
return SomeObject.create <- actually don't know what to put here, i tried json[0], and decode(json[0]) and casting but still no luck
}
What is the correct way to parse that kind of JSON data?

For your information:
let ar = ["1", "string", "", "0.16"]
public struct SomeObject {
public var id: String?
public var name: String?
public var surname: String?
public var length: Float?
}
extension SomeObject {
static func create(id: String?, name: String?, surname: String?, length: Float?) -> SomeObject {
return SomeObject(id: id, name: name, surname: surname, length: length)
}
public static func decode(json: AnyObject?) -> SomeObject {
let js = json as! Array<AnyObject>
return SomeObject.create(js[0] as? String, name: js[1] as? String, surname: js[2] as? String, length: js[3] as? Float)
}
}
let someObject = SomeObject.decode(ar)

Argo isn't setup to pull out certain indices from arrays like this. What you'll have to do is first decode it to a [String] then pick out the indices you want.
let values: Decoded<[String]> = decodeArray(json)
return SomeObject.create
<^> ({ $0[0] } <^> values)
<*> ({ $0[1] } <^> values)
<*> ({ $0[2] } <^> values)
<*> (values >>- { .fromOptional(Float($0[3])) })
You can see that I use closures to pull out the required index. The last one also casts the String to a Float to match your types.
Besides this parsing code, you could also improve the model by using let instead of var to make it immutable as well as use the Curry framework (https://github.com/thoughtbot/Curry) instead of creating your own curried initializer.

Related

Swift - ObjectMapper: Mapping jsonString

I have two models:
class CellModel: StaticMappable {
static func objectForMapping(map: Map) -> BaseMappable? {
return nil
}
func mapping(map: Map) {
id <- map["id"]
title <- map["title"]
description <- map["description"]
}
private var id: Int
private var title: String
private var description: String
init(id: Int, title: String, description: String) {
self.id = id
self.title = title
self.description = description
}
}
and
class CellModelArray: StaticMappable {
var cells = [CellModel]()
static func objectForMapping(map: Map) -> BaseMappable? {
return nil
}
func mapping(map: Map) {
cells <- map["cells"]
}
}
I created a jsonString from object like this:
let jsonString = Mapper().toJSONString(rootModel, prettyPrint: false)
and json looks like this:
{"cells":[{"id":0,"title":"Header","description":"Description"},{"description":"Description","id":0,"title":"Header"},{"description":"Description","id":1,"title":"Header"},{"id":0,"title":"Header","description":"Description"},{"description":"Description","title":"Header","id":0}]}
Then I want to take this string and convert it back to the object, but when I try it like that:
var cells = CellModelArray()
cells = Mapper<CellModelArray>().map(JSONString: code) ?? CellModelArray()
it does not work and returns nil. Thank you for your help.
Please drop ObjectMapper.
It's a library of merit but since Swift 4 (introduced 2017) it has become obsolete in favor of the built-in and swiftier Codable protocol.
The main benefit is that the model files can be structs (value types) and it's much more reliable because the developer doesn't have to take care of literal string keys
This is sufficient
struct Model: Codable {
let cells : [Cell]
}
struct Cell: Codable {
let id: Int
let title: String
let description: String
}
The given JSON string
let jsonString = """
{"cells":[{"id":0,"title":"Header","description":"Description"},{"description":"Description","id":0,"title":"Header"},{"description":"Description","id":1,"title":"Header"},{"id":0,"title":"Header","description":"Description"},{"description":"Description","title":"Header","id":0}]}
"""
can be decoded and encoded with this code
do {
// Decode the JSON
let decoded = try JSONDecoder().decode(Model.self, from: Data(jsonString.utf8))
print(decoded)
// Encode it back
let encoded = try JSONEncoder().encode(decoded)
print(String(data: encoded, encoding: .utf8)!)
} catch {
print(error)
}

Swift 5 Parsing strange json format

I'm trying to parse JSON but keep getting incorrect format error. The JSON I get back from FoodData Central (the USDA's Nutrition API) is as follows:
{
dataType = "Survey (FNDDS)";
description = "Chicken thigh, NS as to cooking method, skin not eaten";
fdcId = 782172;
foodNutrients = (
{
amount = "24.09";
id = 9141826;
nutrient = {
id = 1003;
name = Protein;
number = 203;
rank = 600;
unitName = g;
};
type = FoodNutrient;
},
{
amount = "10.74";
id = "9141827";
nutrient = {
id = 1004;
name = "Total lipid (fat)";
number = 204;
rank = 800;
unitName = g;
};
type = FoodNutrient;
}
);
}
My Structs:
struct Root: Decodable {
let description: String
let foodNutrients: FoodNutrients
}
struct FoodNutrients: Decodable {
// What should go here???
}
From the JSON, it looks like foodNutrients is an array of unnamed objects, each of which has the values amount: String, id: String, and nutrient: Nutrient (which has id, name etc...) However, forgetting the Nutrient object, I can't even parse the amounts.
struct FoodNutrients: Decodable {
let amounts: [String]
}
I don't think its an array of string, but I have no idea what the () in foodNutrients would indicate.
How would I go about parsing this JSON. I'm using Swift 5 and JSONDecoder. To get the JSON I use JSONSerializer, then print out the JSON above.
This is not a JSON. This is a property list in the openStep format.
This is how it can be modelled (use String instead of Int):
struct Root: Decodable {
let description: String
let foodNutrients: [FoodNutrient]
}
struct FoodNutrient: Decodable {
let id: String
let amount: String
let nutrient: Nutrient
}
struct Nutrient: Decodable {
let name: String
let number: String
let rank: String
let unitName: String
}
And then decode it like this:
try PropertyListDecoder().decode(Root.self, from: yourStr)
The () in foodNutrients indicates that it holds an array of objects - in that case FoodNutrient objects. Therefore your root object should look like this:
struct Root: Decodable {
let description: String
let foodNutrients: [FoodNutrient]
}
Now the foodNutrient is except for the nutrient object straightforward:
struct FoodNutrient: Decodable {
let id: Int // <-- in your example it is an integer and in the second object a string, choose the fitting one from the API
let amount: String
let nutrient: Nutrient
}
And the nutrient object should look like this:
struct Nutrient: Decodable {
let name: String
let number: Int
let rank: Int
let unitName: String
}
Using Decodable is a good and easy way to serialize JSON. Hope that helps. Happy coding :)

How to parse several hardcoded keys in JSON API struct with Swift Decodable protocol?

Question: I am trying to decode my JSON, which some of the JSON will have a random string and some will have a hardcoded string. When the hardcoded string is one of the below, I would like to display different UICollectionView Cells. I am having trouble trying to parse my JSON if it is a hardcoded string and being able to display a different UICollectionViewCell with it. Any help on this would be great. This may be a beginner question but I have tried to solve this for this past week and I am having trouble trying to do it. Any help on this would be much appreciated.
** Hardcoded Strings that could be one or the other:**
key: --> This string could be "breaking" or "normal" or "new"
item: --> This string could be "placement" or "slot" or "shared"
verb: --> This string could be "shared" or "posted"
** NOT hardcoded strings, which the string comes in randomly**
heading: --> This string is a random string
type: --> This string is a random string
Here is some of my JSON, so you can get an example of what I am trying to do:
{
slots: [
{
key: "breaking",
item: "placement",
heading: "Random String Text",
type: "Random String Text",
via: "Random",
verb: "shared"
sort_order: 0
},
{
key: "breaking",
item: "placement",
heading: "Random String Text",
type: "Random String Text",
via: "Random",
verb: "posted"
sort_order: 1
},
{
key: "event",
item: "combine",
heading: "Random String Text",
type: "Random String Text",
via: "Random",
verb: "posted"
sort_order: 2
},
}
This is what I have so far for my model:
struct MyModel: Decodable {
var key: String?
var item: String?
var heading: String?
var type: String?
var via: String?
var verb: String?
}
Here is an example cell that Dmitry Serov helped me with.
func collectionView(_ collectionView: UICollectionView,
cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let model = ... // retrieve your model object here
if model.verb == .shared {
// Pass the pertinent identifier
let cell = collectionView.dequeueReusableCell(withReuseIdentifier:...)
return cell
else {
....
}
}
Here is more code that Dmitry Serov helped me with.
struct MyModel { // Type names should use pascal case in Swift
var verb: State?
....
enum State {
case shared
case posted
}
}
// Decode your enums from strings
extension MyModel.State: Decodable {
enum CodingKeys: String, CodingKey {
case shared
case posted
}
}
The issue when I try the above it is asking me to put in the format below which I am not sure how to do and I am trying to parse out several more keys.
extension MyModel.State: Decodable {
init(from decoder: Decoder) throws {
}
}
this is the way I would decode your json using codable:
struct Response: Codable {
var slots = [Slots]()
}
struct Slots: Codable {
var key: String?
var item: String?
var heading: String?
var type: String?
var via: String?
var verb: String?
var order: String?
enum CodingKeys: String, CodingKey {
case order = "sort_order"
}
/** Custom Encoder to send null values**/
func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try? container.encode(order, forKey: .order)
}
}
To compare with hardcoded strings create an enum:
enum key: String {
case breking
case normal
case new
}
use it like this:
if let response = try? JSONDecoder().decode(Response.self, from: response.data!){
for slot in response.slots{
if slot.key == key.breaking.rawValue{
//do something
}
}
}

How to map different type using ObjectMapper?

I'm using ObjectMapper to map my JSON to Swift object.
I have the following Swift object:
class User: Mappable {
var name: String?
var val: Int?
required init?(map: Map) { }
func mapping(map: Map) {
name <- map["name"]
val <- map["userId"]
}
}
I have this JSON structure:
{
"name": "first",
"userId": "1" // here is `String` type.
},
{
"name": "second",
"userId": 1 // here is `Int` type.
}
After mapping the JSON, the userId of User which name is "first" is null.
How can I map Int/String to Int?
After reading the code of ObjectMapper, I found an easier way to solve the problem, it's to custom the transform.
public class IntTransform: TransformType {
public typealias Object = Int
public typealias JSON = Any?
public init() {}
public func transformFromJSON(_ value: Any?) -> Int? {
var result: Int?
guard let json = value else {
return result
}
if json is Int {
result = (json as! Int)
}
if json is String {
result = Int(json as! String)
}
return result
}
public func transformToJSON(_ value: Int?) -> Any?? {
guard let object = value else {
return nil
}
return String(object)
}
}
then, use the custom transform to the mapping function.
class User: Mappable {
var name: String?
var userId: Int?
required init?(map: Map) { }
func mapping(map: Map) {
name <- map["name"]
userId <- (map["userId"], IntTransform()) // here use the custom transform.
}
}
Hope it can help others who have the same problem. :)
If your API is like this - it is very bad API. What you could do is have two variables instead of one:
class User: Mappable {
var name: String?
var valInt: Int?
var valString: String?
required init?(map: Map) { }
func mapping(map: Map) {
name <- map["name"]
valInt <- map["val"]
valString <- map["val"]
}
}
You can even add function that will get you value like:
// bellow func mapping(map: Map){
func getUserId() -> String {
if(self.valInt != nil) {
return "\(valInt!)"
}
else {
return valString!
}
}
Or, using if let:
func getUserId() -> String {
if let userId = self.valInt {
return "\(userId)"
}
if let userId = valString {
return userId
}
return ""
}
Or using optionals so later on you can use if let userId = object.getUserId()
func getUserId() -> String? {
if(self.valInt != nil) {
return String(valInt)
}
else {
return valString
}
}
You should improve your API. However, if you can't do that, then try this code:
class User: Mappable {
var name: String?
var val: Int?
required init?(map: Map) { }
func mapping(map: Map) {
name <- map["name"]
// 1: Get the value of JSON and store it in a variable of type Any?
var userIdData: Any?
userIdData <- map["userId"]
// 2: Check the value type of the value and convert to Int type
if userIdData is Int {
val = (userIdData as! Int)
} else if userIdData is String {
val = Int(userIdData as! String)
}
}
}
You can refer to this document: Type Casting

Typecasting a GenericType that conforms with a Protocol

I'm trying to use an operator to parse a JSON into an object that conforms with the protocol mappable.
First I have a class called Map that is which it's function is store the current key it's being parsed and the complete JSON to parse and the current value to be stored
private class Map {
var json: [String : Any]?
var key: String!
convenience init(json: [String : Any]?) {
self.init()
self.json = json
}
public var currentValue: Any? {
get{
guard let key = key, let json = json else {
return nil
}
return json[key]
}
}
public subscript(key: String) -> Map {
// save key and value associated to it
self.key = key
return self
}
}
Then I have an operator that get the current value on the rhs based on the Map.Key and passes it to the lhs, and here lies the problem if the lhs is an Array with an object that follows the protocol Mappable, it should use the the operator recursively, until end parsing.
The problem is: the comparison lhs is Array always returns false, so I can't typecast it to do the necessary iteration to parse it. (commented lines).
Any ideas how to approach the problem?
infix operator <<< // JSON to object
private func <<< <T: Any>(lhs: inout T, rhs: Map) {
if let rhsArray = rhs.currentValue as? Array<Dictionary<String, Any>>{
print(type(of: lhs)) // <Array<(Book in *someHash*)>>
debugPrint(lhs is Array<Mappable>) // false
var rhsReturnArray: Array<Mappable> = []
for currentRHSValue in rhsArray {
// let object = T.Element.self.from(Map(json: currentRHSValue))
// rhsReturnArray.append(object)
}
// Do something
}
else if let value = rhs.currentValue as? T{
lhs = value
}
}
private class Autor: Mappable {
var books: [Book]?
func from(map: Map) -> Bool {
books <<< map["Books"]
return true
}
}
private class Book: Mappable {
var title: String?
var numberOfPages: Int?
func from(map: Map) -> Bool {
title <<< map["Title"]
numberOfPages <<< map["Number of Pages"]
return true
}
}
let someDictionary = ["Books" : [["Title": "Happy Potter", "Number of Pages": 420], ["Title": "Happy Potter2", "Number of Pages": 422]]]
private let autor = Autor()
autor.from(map: Map(json: someDictionary))
print(autor.books?.first)
And I can't use third part frameworks on this project.