问题
I am working with rest kit 0.20.3 for some project now without issues. Right now I am facing the problem that the JSON keys have leading "@" which causes Restkit to crash while mapping the data.
The JSON looks like this:
{
"city": {
"@name": "Charles Redirection City",
"@nameUrl": "Charles Redirection City",
"lat": "52.5238",
"long": "13.4119",
"zipCode": "666"
},
"categories": {
"@count": "20037",
"category": [
{
"@count": "2326",
"@hasChildren": "true",
"id": "15777",
"name": "Sticky-Categorie"
}
]
},
"additional": {
"dataKey": [
{
"@key": "log-start",
"$": "home"
}
]
}
}
While mapping the app crashes with
*** Terminating app due to uncaught exception 'NSUnknownKeyException', reason: '[<__NSCFDictionary 0x95b5360> valueForUndefinedKey:]: this class is not key value coding-compliant for the key hasChildren.'
as it tries to call hasChildren on the dictionary. My mapping look like this:
[mapping addAttributeMappingsFromDictionary:@{
@"id" : @"categoryId",
@"name" : @"name",
@"@hasChildren" : @"doesHaveChildren",
@"@count" : @"numbersOfJobs",
@"trackingName" : @"trackingString"
}];
I already tried to apply my own mapping which unfortunately didn't work. Is a leading @ not supported in RestKit? I already parsed the JSON with NSJSONSerialization which worked fine but I really love the mapping feature of RestKit and would like to avoid writing my own mapping :)
回答1:
The @
is used in Key-Value Coding:
Collection operators are specialized key paths that are passed as the parameter to the
valueForKeyPath:
method. The operator is specified by a string preceded by an at sign (@
).
I don't know of a way to tell NSDictionary
to ignore it. You could remove the @
from your JSON, either on the server end, or after you receive it but before you hand it to RestKit's mapper.
回答2:
If you can't replace the keys removing the @, there is another solution to test for.
It could be a little bit drastic, but you can try to subclass NSDictionary to replace the implementation of the -(id)valueForKeyPath:(NSString *)keyPath
method.
I say that it is drastic because subclassing a class cluster (like NSDictionary
is never easy, and you will have to do many tests to be sure that RestKit doesn't complain about this.
Moreover, this won't work if RestKit copies the dictionary in a "custom" manner (for example constructing from zero a brand new NSDictionary instead of using copy on our custom subclass).
The goal is to have a subclass that implements a method like this
-(id)valueForKeyPath:(NSString *)keyPath
{
if ([[keyPath substringToIndex:1] isEqualToString:@"@"])
{
return [_subDictionary objectForKey:keyPath]; // if the first character is @, return the result of objectForKey
} else {
return [_subDictionary valueForKeyPath:keyPath];
}
}
You can change the method to return the value of objectForKey
, instead of the value of valueForKeyPath
only in some situations (if the key is "@hasChildren" for example).
This will give you this result:
self.aDictionary = [[FLDictionary alloc] initWithObjects:@[@"pluto"] forKeys:@[@"@pippo"]];
NSLog(@"Value is %@", [self valueForKeyPath:@"aDictionary.@pippo"]);
NSLog result: 2014-06-18 19:24:34.605 Swift123[25663:454958] Value is pluto
However, now comes the difficult part. To subclass NSDictionary, a class cluster, you'll have to override these methods:
-(instancetype)initWithObjects:(const id [])objects forKeys:(const id<NSCopying> [])keys count:(NSUInteger)cnt;
-(NSUInteger)count;
-(id)objectForKey:(id)aKey;
-(NSEnumerator *)keyEnumerator;
Override means that you should implement your own storage for the new created dictionary. To avoid this, I created an instance variable with an underlying NSDictionary. I'll post the complete code at the end of the post.
Another thing to take note: it's a convention to copy NSDictionaries.
This would cause your custom class to be turned in a standard NSDictionary at the first copy.
For this reason, you have even to subclass the copyWithZone:
method.
Here a full working example:
//
// FLDictionary.m
// Swift123
//
// Created by Lombardo on 18/06/14.
// Copyright (c) 2014 Lombardo. All rights reserved.
//
import "FLDictionary.h"
@interface FLDictionary ()
@property (copy, nonatomic) NSDictionary *subDictionary;
@end
@implementation FLDictionary
-(instancetype)initWithObjects:(const id [])objects forKeys:(const id<NSCopying> [])keys count:(NSUInteger)cnt
{
self = [self init];
if (self) {
_subDictionary = [[NSDictionary alloc] initWithObjects:objects forKeys:keys count:cnt];
}
return self;
}
-(NSUInteger)count
{
return [_subDictionary count];
}
-(id)objectForKey:(id)aKey
{
return [_subDictionary objectForKey:aKey];
}
-(NSEnumerator *)keyEnumerator
{
return [_subDictionary keyEnumerator];
}
-(id)valueForKeyPath:(NSString *)keyPath
{
if ([[keyPath substringToIndex:1] isEqualToString:@"@"])
{
return [_subDictionary objectForKey:keyPath];
} else {
return [_subDictionary valueForKeyPath:keyPath];
}
}
-(id)copyWithZone:(NSZone *)zone
{
id objects[ [_subDictionary count] ];
id keys[ [_subDictionary count] ];
int i = 0;
for (id item in [_subDictionary allValues])
{
objects[i++] = item;
}
i = 0;
for (id item in [_subDictionary allKeys])
{
keys[i++] = item;
}
FLDictionary *dict = [[[self class] allocWithZone:zone] initWithObjects:objects forKeys:keys count:[_subDictionary count]];
return dict;
}
@end
回答3:
the idea is that e.g. re-building the dictionary with new keys, which won't hold any @
character. that is how you can read values in an NSDictionary
which has @
characters in keys.
I just replace the @
in keys with _
on the top level here for the new dictionary:
NSDictionary *_dictionary; // your original dictionary
NSMutableDictionary *_validatedDictionary = [NSMutableDictionary dictionary];
[_dictionary enumerateKeysAndObjectsUsingBlock:^(NSString * key, id obj, BOOL *stop) {
NSString *_validKey = [key stringByReplacingOccurrencesOfString:@"@" withString:@"_"];
[_validatedDictionary setObject:obj forKey:_validKey];
}];
NOTE: that is just a concept idea which works on the top-level only in my code, you may use it to create a recursive algorithm to replace deeper levels.
回答4:
Thank you for the contributions. After wiggling around for some hours I followed @holey and @LombaX advice to replace the result. A simply but brilliant approach!
I ended up in replacing the received JSON once it available by simply calling a replace i.e.
NSString *responseStringer = operation.HTTPRequestOperation.responseString;
responseStringer = [responseStringer stringByReplacingOccurrencesOfString:@"\"@" withString:@"\""];
NSData *data = [responseStringer dataUsingEncoding:NSUTF8StringEncoding];
NSError* error;
id parsedData = [RKMIMETypeSerialization objectFromData:data MIMEType:@"application/json" error:&error];
passing the result (parsedData) again to RestKit for mapping:
NSDictionary *mappingsDictionary = @{ @"jobCategories.category": MY_RKObjectMapping_MAPPING_OBJECT };
RKMapperOperation *mapper = [[RKMapperOperation alloc] initWithRepresentation:parsedData mappingsDictionary:mappingsDictionary];
NSError *mappingError = nil;
BOOL isMapped = [mapper execute:&mappingError];
if (isMapped && !mappingError) { ... }
Thank you so much for your help and pointing me into the right direction!
来源:https://stackoverflow.com/questions/24290577/mapping-of-json-keys-with-leading-with-restkit