I want to use Mantle to serialize some objects to this JSON:
{
"name": "John Smith",
"age": 30,
"department_id":123
}
I have two classes Department Employee:
#import <Mantle/Mantle.h>
#interface Department : MTLModel <MTLJSONSerializing>
#property(nonatomic)int id;
#property(nonatomic)NSString *name;
#end
and the Employee class:
#import <Mantle/Mantle.h>
#import "Department.h"
#interface Employee : MTLModel <MTLJSONSerializing>
#property(nonatomic)NSString *name;
#property(nonatomic)int age;
#property(nonatomic)Department *department;
#end
#implementation Employee
+ (NSDictionary *)JSONKeyPathsByPropertyKey {
return #{
#"name":#"name",
#"age":#"age",
#"department.id":#"department_id"
};
}
#end
when serializing an Employee instance I receive the following
exception: "NSInternalInconsistencyException", "department.id is not a
property of Employee."
What's wrong here? is there a way to serialize the object as as a single dictionary instead of nesting the department object inside the employee object?
first remove this code from your Employee.m file
#implementation Employee
+ (NSDictionary *)JSONKeyPathsByPropertyKey {
return #{
#"name":#"name",
#"age":#"age",
#"department.id":#"department_id"
};
}
and then use the following whenever you want to serialize the Employee object
Employee *objEmployee = [Employee instanceFromDict:responseObject];
I hope it will work for you. All the best!!
OK, I got it from here:
Mantle property class based on another property?
I modified the mapping dictionary to be like this
+ (NSDictionary *)JSONKeyPathsByPropertyKey {
return #{
#"name":#"name",
#"age":#"age",
NSStringFromSelector(#selector(department)) : #[#"department_id"]
};
}
and added:
+ (NSValueTransformer *)departmentJSONTransformer {
return [MTLValueTransformer transformerUsingReversibleBlock:^id(Department *department, BOOL *success, NSError *__autoreleasing *error) {
return [MTLJSONAdapter JSONDictionaryFromModel:department error:nil];
}];
}
Related
Hello everyone i am new to MTLModel and trying to understand it correctly. I am making a network query and retrieve a NSDictionary json data.
Data include an array of objects. There are two types of objects in the array mixed.
I use the following line of code to create my model.
HashTagsContentTilesResult *model = [MTLJSONAdapter modelOfClass:[HashTagsContentTilesResult class] fromJSONDictionary:response error:&error];
The model returns the transformed data but all objects are transformed using one class. I need to transform each one item that is included in the data dictionary using a different class because they have a different type of properties. This is my code in the MTLmodel .m file
#implementation C8MHashTagsContentTilesResultForOtherTiles
+ (NSDictionary *)JSONKeyPathsByPropertyKey {
return
#{
#"total" : #"totalTermCount",
#"data" : #"data"
};
}
+ (NSValueTransformer *)dataJSONTransformer {
return [MTLJSONAdapter arrayTransformerWithModelClass:C8MHashTagsContentItemForOtherTiles.class];
}
#end
#implementation C8MHashTagsContentTilesResult
+ (NSDictionary *)JSONKeyPathsByPropertyKey {
return
#{
#"total" : #"totalTermCount",
#"data" : #"data"
};
}
+ (NSValueTransformer *)dataJSONTransformer {
return [MTLJSONAdapter arrayTransformerWithModelClass:C8MHashTagsContentItem.class];
}
+ (Class)classForParsingJSONDictionary:(NSDictionary *)JSONDictionary {
for (NSDictionary *dictionary in JSONDictionary[#"data"])
{
if ([dictionary[#"feedType"] isEqualToString:#"creator_content"]) {
NSLog(#"mtlmodel: creators content");
return C8MHashTagsContentTilesResult.class;
}
else{
NSLog(#"mtlmodel: other content");
return C8MHashTagsContentTilesResultForOtherTiles.class;
}
}
NSAssert(NO, #"No matching class for the JSON dictionary '%#'.", JSONDictionary);
return self;
}
The above code returns the data but uses the same class for all items in the dictionary. How can i use a different class for each item in the dictionary and when all items in the dictionary are transformed one by one...then return all the data? Any help appreciated.
I am receiving below JSON response for saving user preferences in core data.
preferences={
1={
children=({
3=Samsung;4=Nokia;
});id=1;name=Mobiles;
};2={
children=({
5="Samsung Curve TV";
});id=2;name=Electronics;
};
};
Here is my code snippet which is working fine. But I think this is much verbose code.
NSLog(#"Preferences: %#", [response objectForKey:#"preferences"]);
for (NSDictionary *dic in [response objectForKey:#"preferences"]) {
NSLog(#"ID: %#", [[[response objectForKey:#"preferences"] objectForKey:dic] objectForKey:#"id"]);
NSLog(#"NAME: %#", [[[response objectForKey:#"preferences"] objectForKey:dic] objectForKey:#"name"]);
NSLog(#"Children DIC: %#", [[[[[response objectForKey:#"preferences"]
objectForKey:dic] objectForKey:#"children"] objectAtIndex:0] objectForKey:#"3"]);
for (NSDictionary *childDic in [[[[response objectForKey:#"preferences"]
objectForKey:dic] objectForKey:#"children"] objectAtIndex:0]) {
NSLog(#"Child Name: %#", [[[[[response objectForKey:#"preferences"]
objectForKey:dic] objectForKey:#"children"] objectAtIndex:0] objectForKey:childDic]);
}
}
I have 3 questions.
How can I improve my code snippet? is there any shorter way to implement this?
Is this JSON response is good enough for mobile end parsing? is it good JSON format? Is there any JSON response format we should follow as Mobile developers in terms of using Core data (which simply reduces DB implementation as best practice)?
How do I construct a JSON string like this again from Objective-c?
Ok so, (Sorry for NOT deep analysis) first of all I'd modify your JSON not to contain dictionary for dynamic elements (samsung has key 3 and so on, it should be array, make sense?)
I come up with imho better JSON structure:
{
"preferences":[
{
"items":[
{
"id" : "3",
"name" : "Samsung"
},
{
"id" : "3",
"name" : "Nokia"
}
],
"id":"1",
"name":"Mobiles"
},
{
"items":[
{
"id" : "3",
"name" : "Nokia"
}
],
"id":"2",
"name":"Electronics"
}
]
}
It's really easy now to map it to objects with JSONModel and only one thing you should care about is mapping keys.
NSString *JSONString = #" { \"preferences\":[ { \"items\":[ { \"id\" : \"3\", \"name\" : \"Samsung\" }, { \"id\" : \"3\", \"name\" : \"Nokia\" } ], \"id\":\"1\", \"name\":\"Mobiles\" }, { \"items\":[ { \"id\" : \"3\", \"name\" : \"Nokia\" } ], \"id\":\"2\", \"name\":\"Electronics\" } ] }";
NSDictionary *dictionary = [NSJSONSerialization JSONObjectWithData:[JSONString dataUsingEncoding:NSUTF8StringEncoding] options:0 error:nil];
NSError *mapError;
GlobalPreferences *globalPreferences = [[GlobalPreferences alloc] initWithDictionary:dictionary error:&mapError];
if (mapError) {
NSLog(#"Something went wrong with mapping model %#", mapError);
}
NSLog(#"Your mapped model: %#", globalPreferences);
Models:
Kinda GlobalPreference, root model, in case if u decide to add something extra
#import <JSONModel/JSONModel.h>
#import "Preference.h"
#interface GlobalPreferences : JSONModel
#property (nonatomic, strong) NSArray<Preference> *preferences; // using protocol here you specifying to which model data should be mapped
#property (nonatomic, strong) NSArray<Optional> *somethingElse; // some other settings may be here
#end
#import "GlobalPreferences.h"
#implementation GlobalPreferences
#end
Preference
#import <JSONModel/JSONModel.h>
#import "PreferenceItem.h"
#protocol Preference <NSObject>
#end
#interface Preference : JSONModel
#property (nonatomic, strong) NSNumber *ID; // required
#property (nonatomic, strong) NSString *name; // required
#property (nonatomic, strong) NSArray<PreferenceItem> *items;
#end
#import "Preference.h"
#implementation Preference
#pragma mark - JSONModel
+ (JSONKeyMapper *)keyMapper {
return [[JSONKeyMapper alloc] initWithDictionary:#{
#"id": #"ID",
}];
}
#end
PreferenceItem
#import <JSONModel/JSONModel.h>
// This protocol just to let JSONModel know to which model needs to be parsed data in case if it's an array/dictionary
#protocol PreferenceItem <NSObject>
#end
#interface PreferenceItem : JSONModel
#property (nonatomic, strong) NSNumber *ID;
#property (nonatomic, strong) NSString *name;
#end
#import "PreferenceItem.h"
#implementation PreferenceItem
#pragma mark - JSONModel
+ (JSONKeyMapper *)keyMapper {
return [[JSONKeyMapper alloc] initWithDictionary:#{
#"id": #"ID",
}];
}
#end
Should be ok for coreData.
Maybe for you case all this is not necessary, but there are lot of things you need to care when u parsing/mapping network response such es data types, missing keys, error handling and so on. If u map it manually - you risk to have broken app some day.
I know you asked best practices for Objective-C, but I would suggest switching to Swift and using SwiftyJSON. With Swift the code is much more readable than with Objective-C.
let prefs = json["preferences"]
let userName = prefs["id"].stringValue
let name = prefs["name"].stringValue
let child = prefs["children"].arrayValue[0].arrayValue[3]
https://github.com/SwiftyJSON/SwiftyJSON
You can easily build your own JSON structures and send them with Alamofire:
let parameters: [String : AnyObject] = [
"preferences": [
"id": "123",
"name": "John",
"children": [
["3": "Three"]
]
]
]
let urlReq = NSMutableURLRequest(url)
urlReq.HTTPMethod = .POST
let req = Alamofire.ParameterEncoding.JSON.encode(urlReq, parameters: parameters).0
let request = Alamofire.request(req)
https://github.com/Alamofire/Alamofire
I'm trying to get the subnodes of a JSON file usning githubs mantle.
This is what I tried:
JSON
"countries": {
"name": "germany",
"population": "80620000000",
"populationInCities": {
"Hamburg": 1799000,
"Berlin": 3502000,
"Munich": 1378000
}
}
CountryInfo.h
#import <Mantle/Mantle.h>
#interface CountryInfo : MTLModel <MTLJSONSerializing>
#property (nonatomic, readonly, copy) NSString *collectionName;
#property (nonatomic, readonly, assign) NSUInteger cPopulation;
#property (nonatomic, readonly, copy) NSArray *populationInCities;
#end
CountryInfo.m
+ (NSDictionary *)JSONKeyPathsByPropertyKey {
return #{ #"cName": #"name",
#"cPopulation": #"population",
#"populationInCities": [NSSet setWithArray:#[ #"Hamburg", #"Hamburg", #"Hamburg"]]
};
}
+ (NSValueTransformer *)populationInCitiesJSONTransformer {
return [NSValueTransformer mtl_JSONArrayTransformerWithModelClass:CountryInfo.class];
}
I'm getting an error when I run my APP:
*** Terminating app due to uncaught exception 'NSInternalInconsistencyException', reason: 'populationInCities must either map to a JSON key path or NSNull, got: {
populationInCities = (
Hamburg,
Berlin,
Munich
);
}.'
If you want to store population numbers in populationInCities array, you need a custom transformer:
+ (NSDictionary *)JSONKeyPathsByPropertyKey {
return #{#"collectionName": #"name",
#"cPopulation": #"population"};// 'populationInCities' names in JSON and model are the same, so no need to include here.
}
+ (NSValueTransformer *)populationInCitiesJSONTransformer {
return [MTLValueTransformer transformerWithBlock:^(NSDictionary *dict) {
return [dict allValues];
}];
}
Embarrassing, but it was simly working using .-notation.
e.g.
popukationInCities.hamburg
I have MyModel inheriting from MTLModel (using the GitHub Mantle pod).
MyModel.h
#import <Mantle/Mantle.h>
#interface MyModel : MTLModel <MTLJSONSerializing>
#property (nonatomic, copy, readonly) NSString *UUID;
#property (nonatomic, copy) NSString *someProp;
#property (nonatomic, copy) NSString *anotherProp;
#end
MyModel.m
#import "MyModel.h"
#implementation MyModel
+ (NSDictionary *)JSONKeyPathsByPropertyKey
{
return #{
#"UUID": #"id",
#"someProp": #"some_prop",
#"anotherProp": #"another"
};
}
}
#end
Now I want to send the JSON to the backend using AFNetworking. Before that I convert the model instance to a JSON NSDictionary to use as parameters/body payload within my request.
NSDictionary *JSON = [MTLJSONAdapter JSONDictionaryFromModel:myModel];
But this JSON consists of strange "" Strings for properties of my model that are nil. What i instead want is Mantle to omit these key/value pairs and just spit out a JSON with only the properties that are not nil or NSNull.null, whatever.
This is a common issue with Mantle and it's called implicit JSON mapping.
MTLJSONAdapter reads all properties of a model to create a JSON string optionally replacing property names with ones given in +JSONKeyPathsByPropertyKey.
If you want some properties to be excluded from the JSON representation of your model, map them to NSNull.null in your +JSONKeyPathsByPropertyKey:
+ (NSDictionary *)JSONKeyPathsByPropertyKey {
return #{
#"UUID": #"id",
#"someProp": #"some_prop",
#"anotherProp": #"another",
#"myInternalProperty": NSNull.null,
#"myAnotherInternalProperty": NSNull.null,
};
}
The implicit JSON mapping has lately become a noticeable problem, a solution for which is currently being discussed at Mantle's home repository at GitHub.
See issues #137, #138, #143 and the current discussion under #149.
EDIT: I clearly misunderstood the question, but now, when I suppose I understand it correctly, the answer is simple.
MTLJSONAdapter generates the JSON data using MTLModel's dictionaryValue property. If you wish to exclude a property from the JSON itself, you can overwrite that method in your MYModel:
- (NSDictionary *)dictionaryValue {
NSMutableDictionary *originalDictionaryValue = [[super dictionaryValue] mutableCopy];
if (self.aPropertyThatShouldBeExcludedWhenNil == nil) {
[originalDictionaryValue removeObjectForKey:#"aPropertyThatShouldBeExcludedWhenNil"];
}
/* repeat the process for other "hidden" properties */
return originalDictionaryValue;
}
EDIT #2: Check out the code* for removing all values that are nil:
- (NSDictionary *)dictionaryValue {
NSMutableDictionary *modifiedDictionaryValue = [[super dictionaryValue] mutableCopy];
for (NSString *originalKey in [super dictionaryValue]) {
if ([self valueForKey:originalKey] == nil) {
[modifiedDictionaryValue removeObjectForKey:originalKey];
}
}
return [modifiedDictionaryValue copy];
}
* - code sample suggested by matths.
I remove nil valued keys by creating an MTLJSONAdapter subclass, and overriding -serializablePropertyKeys:forModel: method.
MTLJSONAdapterWithoutNil.h
/** A MTLJSONAdapter subclass that removes model dictionaryValue keys whose value is `[NSNull null]`. */
#interface MTLJSONAdapterWithoutNil : MTLJSONAdapter
#end
MTLJSONAdapterWithoutNil.m
#import "MTLJSONAdapterWithoutNil.h"
#implementation MTLJSONAdapterWithoutNil
- (NSSet *)serializablePropertyKeys:(NSSet *)propertyKeys forModel:(id<MTLJSONSerializing>)model {
NSMutableSet *ms = propertyKeys.mutableCopy;
NSDictionary *modelDictValue = [model dictionaryValue];
for (NSString *key in ms) {
id val = [modelDictValue valueForKey:key];
if ([[NSNull null] isEqual:val]) { // MTLModel -dictionaryValue nil value is represented by NSNull
[ms removeObject:key];
}
}
return [NSSet setWithSet:ms];
}
#end
And use this to create JSON dictionary instead. Like this:
NSDictionary *JSONDictionary = [MTLJSONAdapterWithoutNil JSONDictionaryFromModel:collection error:nil];
NOTE: if you are overriding NSValueTransformer methods for array or dictionary properties, you also have to change the MTLJSONAdapter class to your subclass as well. Like this:
+ (NSValueTransformer *)myDailyDataArrayJSONTransformer {
return [MTLJSONAdapterWithoutNil arrayTransformerWithModelClass:KBDailyData.class];
}
Overriding - dictionaryValues did not give me the expected behavior
So I created a method for MTL Base class
- (NSDictionary *)nonNullDictionaryWithAdditionalParams:(NSDictionary *)params error:(NSError *)error {
NSDictionary *allParams = [MTLJSONAdapter JSONDictionaryFromModel:self error: &error];
NSMutableDictionary *modifiedDictionaryValue = [allParams mutableCopy];
for (NSString *originalKey in allParams) {
if ([allParams objectForKey:originalKey] == NSNull.null) {
[modifiedDictionaryValue removeObjectForKey:originalKey];
}
}
[modifiedDictionaryValue addEntriesFromDictionary:params];
return [modifiedDictionaryValue copy];
}
The EDIT #2 used to work for me with the previous Mantle code base. Now I have to do the following to continue using EDIT #2:
In file MTLJSONAdapter.m, replace this line:
NSDictionary *dictionaryValue = [model.dictionaryValue dictionaryWithValuesForKeys:propertyKeysToSerialize.allObjects];
with
NSDictionary *dictionaryValue = model.dictionaryValue;
The above is my current workaround to get
{ }
instead of
{
"AddressLine2" : null,
"City" : null,
"ZipCode" : null,
"State" : null,
"AddressLine1" : null
}
If I have a dictionary like
{
name: "Bob",
cars: [
{ make: "ford", year: "1972" },
{ make: "mazda", year: "2000" }
],
}
and two models like:
#interface CarModel : MTLModel
#property (nonatomic, strong) NSString *make;
#property (nonatomic, strong) NSString *year;
#end
#interface PersonModel : MTLModel
#property (nonatomic, strong) NSString *name;
#property (nonatomic, strong) NSArray *cars;
#end
How do I use Mantle so that my array of cars in my person model are CarModels?
Ah figured it out. I needed to add a private method:
+ (NSValueTransformer *)carsTransformer
{
return [NSValueTransformer mtl_externalRepresentationArrayTransformerWithModelClass:[CarModel class]];
}
and make sure I used:
[PersonModel modelWithExternalRepresentation:dict];
+[NSValueTransformer mtl_externalRepresentationArrayTransformerWithModelClass:] is deprecated. The new API is +[NSValueTransformer mtl_JSONArrayTransformerWithModelClass:].
After switching to the new API, the models can be initialized with the default initializers provided by, e.g., MTLJSONAdapter.
A note on:
[NSValueTransformer mtl_JSONArrayTransformerWithModelClass:CarModel.class];
This methods seems to now be deprecated. I'm using this new method in my code and it appears to be working just fine:
[MTLJSONAdapter arrayTransformerWithModelClass:CarModel.class];
+ (NSValueTransformer *)carsJSONTransformer {
return [NSValueTransformer mtl_JSONArrayTransformerWithModelClass:CarModel.class];
}
Read more here
+[NSValueTransformer mtl_JSONArrayTransformerWithModelClass:]
This method is deprecated. New method is:
+ (NSValueTransformer *)carsTransformer
{
return [MTLJSONAdapter arrayTransformerWithModelClass:[CarsModel class]];
}