My app creates some ObJC objects from JSON. Things were working fine, until I added a new property to my ObjC model class, which doesn't have a counterpart in JSON.
I've configured the mapping as follows:
+ (NSDictionary *)JSONKeyPathsByPropertyKey
{
return #{
#"firstName" : #"firstname",
#"middleName" : #"middlename",
#"lastName" : #"lastname",
// etc.
#"phoneNumber" : NSNull.null // no JSON data for this one
};
}
However I get an assertion failure in Mantle, in MTLJSONAdapter initWithModelClass: "phoneNumber must either map to a JSON key path or a JSON array of key paths, got: null."
This is how I create the model object:
MyData *myData = [MTLJSONAdapter modelOfClass:[MyData class]
fromJSONDictionary:json error:&error];
How can I have the phoneNumber property in the data class without it mapping to a JSON value?
Just don't specify its mapping to + JSONKeyPathsByPropertyKey.
The accepted answer didn't end up working for me (I wasn't parsing from a JSON object) and I found subclassing the encodingBehaviorsByPropertyKey to work. The comments in the header file read as follows:
/// Determines how the +propertyKeys of the class are encoded into an archive.
/// The values of this dictionary should be boxed MTLModelEncodingBehavior
/// values.
///
/// Any keys not present in the dictionary will be excluded from the archive.
///
/// Subclasses overriding this method should combine their values with those of
/// `super`.
///
/// Returns a dictionary mapping the receiver's +propertyKeys to default encoding
/// behaviors. If a property is an object with `weak` semantics, the default
/// behavior is MTLModelEncodingBehaviorConditional; otherwise, the default is
/// MTLModelEncodingBehaviorUnconditional.
+ (NSDictionary *)encodingBehaviorsByPropertyKey;
Overriding that method from within my MTLModel subclass like so ended up working for me:
+ (NSDictionary *)encodingBehaviorsByPropertyKey {
NSMutableDictionary *encodingBehaviorsDictionary = [[super encodingBehaviorsByPropertyKey] mutableCopy];
[encodingBehaviorsDictionary removeObjectForKey:#"propertyToBeOmitted"];
return encodingBehaviorsDictionary;
}
FYI: I'm using Mantle version 2.1.
Related
I have an Aqueduct project using the ORM, with a data model as follows:
class _Thing {
#primaryKey
int id;
String first;
String second;
}
class Thing extends ManagedObject<_Thing> implements _Thing {
#Serialize()
OtherThing get firstAndSecond() {
// return some value computed from first and second
}
#Serialize()
set firstAndSecond(OtherThing firstAndSecond) {
// set first and second based on some computation
}
}
According to the docs for transient properties, annotating with #Serialize() should enable this model to be serialized/deserialized. It also says that properties in ManagedObjects are not persisted, but when I run the server, I get the error:
Data Model Error: Property 'firstAndSecond' on 'Thing' has an unsupported type.
If I remove the #Serialize(), it doesn't try to persist it, but I can't serialize/deserialize this object.
Any suggestions as to why this is happening or how I can control this behaviour?
This should be in the docs -
A Serializable property must be a primitive type (e.g. String, int, double, bool or a Map or List containing these types). Serializable values are passed directly to the codec that is reading from a request body or writing to a response body (by default, this codec is JSON). In the case of a custom type like OtherThing, the codec doesn't know how to encode or decode that type.
For complex types, you might use a map:
#Serialize()
Map<String, dynanic> get firstAndSecond() {
return {"first": first, "second": second};
}
You might also use CSV-like data:
#Serialize()
String get firstAndSecond() {
return "$first,$second";
}
I'm stuck on why my transformable classes aren't being called. The following screenshot shows my Entity attribute as a transformable:
According to the documentation, it should automatically call the class "StringEncryptionTransformer" and perform the transformation.
I followed this guide in order to set up the class. I'm using the EncryptionTransformer and StringEncryptionTransformer classes provided, the only change I made was with the encryption to use RNcryptor.
Encryption:
return [RNEncryptor encryptData:data withSettings:kRNCryptorAES256Settings password:[self key] error:&error];
and Decryption:
return [RNDecryptor decryptData:data withPassword:[self key] error:&error];
The saved entity appears never to go through the transformation, is there something I'm missing? I tried adding an initialize to the NSManagedObject, but the results were the same.
you need to register value transformer like below
extension NSValueTransformerName {
static let classNameTransformerName = NSValueTransformerName(rawValue: "ClassNameTransformer")
}
ValueTransformer.setValueTransformer(ClassNameTransformer(), forName: .classNameTransformerName)
I have a simple question but I can't find a nice solution to it.
I have a signal that sends strings, and a map after it. The map converts the strings into JSON.
It may happen that the string has a wrong format and the JSON parser will fail to parse with an error.
[stringGeneratorSignal map:^(NSString *bussinessObjectString){
NSError *error;
BussinessObject *obj = [[BussinessObject alloc] initWithString:bussinessObjectString error:&error];
if (error) {
NSLog(#"%#", error);
}
return obj;
}];
But because I'm inside the map I can't return an error signal. What I would like is to get an error with the error provided by the parser.
A couple of possibilities I've analyzed that I don't like:
Return the error in the map and then have a wrapper signal that actually convert the data (or error) into an error signal. The problem is that I'm delegating the same problem (converting the data into an error signal).
Use flattenMap instead. This will allow to return an error message, but the problem is that it's not the same behaviour as map.
What is the best approach for this kind of scenarios?
Thanks!
Look at -tryMap. It allows you to return data or nil and then set the error
/// Runs `mapBlock` against each of the receiver's values, mapping values until
/// `mapBlock` returns nil, or the receiver completes.
///
/// mapBlock - An action to map each of the receiver's values. The block should
/// return a non-nil value to indicate that the action was successful.
/// This block must not be nil.
///
/// Example:
///
/// // The returned signal will send an error if data cannot be read from
/// // `fileURL`.
/// [signal tryMap:^(NSURL *fileURL, NSError **errorPtr) {
/// return [NSData dataWithContentsOfURL:fileURL options:0 error:errorPtr];
/// }];
///
/// Returns a signal which transforms all the values of the receiver. If
/// `mapBlock` returns nil for any value, the returned signal will error using
/// the `NSError` passed out from the block.
- (RACSignal *)tryMap:(id (^)(id value, NSError **errorPtr))mapBlock;
I'm looking at the Overcoat library which from what I gather is a library that extends the Mantle library.
Mantle: https://github.com/Mantle/Mantle/
Overcoat: https://github.com/gonzalezreal/Overcoat
The Mantle and Overcoat github pages keeps mentioning about creating a Mantle model but I want to know how do I generate the Mantle model? Do I manually type it out or do I use Xcode xcdatamodel file to build it visually, then generate the sublass + modify that file afterwards?
In Core Data do create the entity in the xcdatamodel file using the Interface Builder, then use Xcode's Editor > Create NSManagedObject subclass.
Do we do the same for Mantle and then change from NSManagedObject to MTLModel ?
What happens when we decided to update the Core Data entity in the xcdatamodel file? If we regenerate the model file again, wouldn't we have to re-add all those changes to the NSManagedObject class?
Super confused about the process.
OK, I think I'm starting to grasp it a bit more. After a couple of hours of trial and error, I was able to get a basic Overcoat demo app working with Core Data pulling from my REST API.
I got it to work like so:
1) We create the Entities inside the xcdatamodel file BUT DO NOT generate the NSManagedObject class using Editor > Create NSManagedObject Classes menu.
2) Create the Mantle model subclass per the normal Right Click project folder (in Xcode) > New File > Choose MTLModel as subclass and then manually entering in the properties. Notably, the subclass header should be something like:
#interface Book : MTLModel <MTLJSONSerializing, MTLManagedObjectSerializing>
#property (nonatomic, copy) NSString *title;
...
#end
3) If you accidentally generated the Core Data entity like me, the xcdatamodel file actually adds the class name in the "Default" section under "Configuration" inside the xcdatamodel.
You need to delete any value in the "Class" column otherwise you end up with a bad crash saying:
"XYZ" is not a subclass of NSManagedObject.
4) Ensure in your Mantle model class you implement the serializing methods for MTLJSONSerialization and MTLManagedObjectSerializing.
#pragma mark - MTLJSONSerialization -
+(NSDictionary *)JSONKeyPathsByPropertyKey
{
return #{
#"title": #"title",
...
};
}
#pragma mark - MTLManagedObjectSerializing -
+(NSString *)managedObjectEntityName
{
// ------------------------------------------------
// If you have a Core Data entity called "Book"
// then you return #"Book";
//
// Don't return the Mantle model class name here.
// ------------------------------------------------
return #"TheCoreDataEntityName";
}
+(NSDictionary *)managedObjectKeysByPropertyKey
{
// ------------------------------------------------
// not really sure what this does, I just put
// it in as the example does it too
// ------------------------------------------------
return #{};
}
These methods essentially is the glue mapping the JSON response from the server to the Core Data Entities.
5) One more important thing that got me is the way the server return responses.
Your server might use HTTP Status code and no top level JSON dictionary
e.g.
// no top level JSON dictionary, purely just an array of results
{
{
title: "ABC",
...
},
{
title: "ABC",
...
},
{
title: "ABC",
...
},
}
Whereas, other types of REST server might return a top level JSON dictionary with the results key path at a sub level like so:
{
count: 20,
next: "http://www.server.com/api/resource?page=2",
previous: null,
results:(
{
title: "ABC",
...
},
{
title: "ABC",
...
},
{
title: "ABC",
...
})
}
In the latter case, that's known as an "Envelop" type of response from my vague understanding. For these type of server responses, there is an extra step that involves telling Overcoat where the array of results key path is in the JSON response.
To do this, we need to:
5a) Create a ServerResponse class that is a subclass of OVCresponse:
// .h file
#import "OVCResponse.h"
#interface ServerResponse : OVCResponse
#end
// .m file
#implementation ServerResponse
+(NSString *)resultKeyPathForJSONDictionary:(NSDictionary *)JSONDictionary
{
// --------------------------------------------------------------------
// we're telling Overcoat, the array of entities is found under the
// "results" key-value pair in the server response JSON dictionary
// --------------------------------------------------------------------
return #"results";
}
#end
5b) In your APIClient class (which should be a subclass of OVCHTTPSessionManager), override the method:
+(Class)responseClass
{
// --------------------------------------------------
// ServerResponse class will let Overcoat know
// where to find the results array
// --------------------------------------------------
return [ServerResponse class];
}
Hopefully this helps anyone else who's having the same problem trying to get Overcoat working.
I need your help with something, as I can't get my head around this. I'm using Mantle together with CoreData in iOS.
I have relationships defined that look as follows:
Post 1:N Comment
When I pull the data from my REST Service, I create a Mantle Object Post that has a NSMutableArray of Comments in it. This works flawlessly.
I then store this in Core Data, and this is where I don't know whether I am doing things right.
[MTLManagedObjectAdapter managedObjectFromModel:post insertingIntoContext:[self getManagedObjectContext] error:&error];
So I'm doing this to store my post object into Core Data. The Core Data Model has a relationship called "post_has_comments" which is a cascading One-to-Many relationship. So on the object Post I have "posts_has_comments" -> cascading, on my object "Comment" I have a one-to-one relationship with "Nullify".
Afaik, Core Data treats this as a NSSet. What I'm trying to put in is a NSMutableArray though, as Mantle will take care of this (at least thats what a quick look in its source told me).
Unfortunately, when I get the object back from Core Data with
Post* post = [MTLManagedObjectAdapter modelOfClass:Post.class fromManagedObject:[[self fetchedResultsController] objectAtIndexPath:indexPath] error:nil];
The property comments on the post object is a empty NSSet, and I get quite some errors upon inserting the thing beforehand. The errors I get:
Core Data: annotation: repairing missing delete propagation for to-many relationship post_has_comments on object [...]
I am stuck - Maybe I am missing something huge here?
My Post Class implements the following static methods:
+ (NSDictionary *)managedObjectKeysByPropertyKey {
return #{
#"post_id" : #"id",
#"comments" : #"post_has_comments"
};
}
+ (NSDictionary *)JSONKeyPathsByPropertyKey {
return #{
#"post_id" : #"id",
};
}
+ (NSDictionary *)relationshipModelClassesByPropertyKey {
return #{
#"comments" : IHComment.class
};
}
A simple workaround is to write your own property setter method and if value being set is NSSet then convert it to NSMutableArray before setting it back to your property ivar.
For example:
- (void)setComments:(NSMutableArray *)comments {
if ([comments isKindOfClass:NSSet.class]) {
_comments = [NSMutableArray arrayWithArray:[((NSSet *)comments) allObjects]];
} else {
_comments = comments;
}
}
I've done it myself quite a few times and it works like a charm!
From the Mantle documentation:
Mantle makes it easy to write a simple model layer for your Cocoa or Cocoa Touch application.
This is simply an unproven statement. Looking at the framework, I do not see where the evidence is. You should get your objects, and insert them into Core Data using Apple's APIs.
Post *cdPost = [NSEntityDescription insertNewObjectForEntityForName:#"Post"
inManagedObjectContext:self.managedObjectContext];
// configure the cdPost object with the data from the web service
for (id commentObject in commentArrayFromPostObject) {
Comment *cdComment =
[NSEntityDescription insertNewObjectForEntityForName:#"Comment"
inManagedObjectContext:self.managedObjectContext];
// configure the cdComment object with the data from the web service
cdComment.post = cdPost;
}
That's all there is to it.