Пользователь
0,0
рейтинг
1 июня 2013 в 14:49

Разработка → Супер простой iOS JSON mapper из песочницы tutorial

Каждый, кто хотя бы раз писал клиент-серверное приложение под iOS, так или иначе сталкивался с маппингом json/xml/прочее в объекты. Иногда это бывает сложно, иногда вообще хочется работать просто со словарями, есть уже много готовых решений типа RestKit, который вообще являет собой универсальный комбайн на все случаи жизни, так зачем же писать очередной велосипед?

Причин может быть много, несколько из них, которые и сподвигли меня написать свой мини-маппер, это:

  • не хочется разбираться с чем-то большим и сложным;
  • мне нужен только маппер, без дополнительных плюшек типа работы с сетью или интеграции с Core Data;
  • если фреймворк работает не так как я хочу, часто разобраться и поправить в нем что-то становится реальной головной болью, особенно если стадия проекта далеко не начальная и отказаться от фреймворка проблемно;
  • мне не нужен в проекте на 3 экрана фреймворк еще на 50 классов и 4 МБ весом;
  • свое всегда роднее.


Первым делом ссылка на GitHub, где лежит исходник базового класса для наших будующих моделей и пример проекта с использованием маппера.

Прежде чем хвалить или еще что-то, опишу минусы:

  • на каждый json объект, какой бы крошечный он не был, придется создать Objective-c класс (хотя может это плюс?);
  • нет никаких проверок входных данных;
  • внимательность разработчика, некоторые вещи не проверяются (нужно мапить массив кастомных объектов, но не указано в какие классы будет оно мапиться, например);
  • еще что-то, доказывающее что маппер абсолютно неправильный и использовать его ни в коем случае нельзя, о чем непременно обоснованно напишут в комментариях.

Из плюсов — простой в использовании, крошечный как по размеру так и по количеству классов, в которых в случае чего придется разбираться, делает свое дело.

Как с ним работать? Маппер состоит из одного класса — TinyMappingModel, это базовый класс для всех последующих классов модели. На каждый объект json создается по наследнику TinyMappingModel, который должен содержать в себе свойства для хранения нужных данных. В идеальном случае стоит называть их так же, как называются соответствующие поля в json — тогда маппинг будет происходить сам по себе, как по волшебству (KVC), о случаях, когда это невозможно (например, поле с название im:name, id, 1work), напишу ниже.

TinyMappingModel содержит в себе 4 метода:

//public
+ (instancetype)mapObjectFromDictionary:(NSDictionary *)data;
+ (NSArray *)mapArrayOfObjects:(NSArray *)data;

//protected methods
- (NSDictionary *)keyToClassMappingRules;
- (NSDictionary *)keyToPropertyNameReplacementRules;

Hаследники по необходимости должны переопределять два последних.

— (NSDictionary *)keyToClassMappingRules; — переопределяется в случае, если нам нужно замапить json объект в какой-то кастомный класс (наследник TinyMappingModel) или в массив. Метод должен возвращать словарь с парами ключ — имя поля в json, значение — класс в который будет мапиться объект или, в случае коллекции, из каких объектов будет состоять коллекция. Например:

- (NSDictionary *)keyToClassMappingRules {
    return @{@"im:name":[TitleModel class], @"im:image":[ImageModel class]};
}

— (NSDictionary *)keyToPropertyNameReplacementRules; — переопределяется в случае, если мы не можем/хотим по каким-то причинам назвать свойство в классе так же, как оно называется в json. Ключ — имя поля в json, значение — название свойства в классе, например:

- (NSDictionary *)keyToPropertyNameReplacementRules {
    return @{@"im:name":@"name",@"im:image":@"images"};
}

Два данных метода из примера будут единственным, что минимально необходимо реализовать в имплементации. В хедере нашего класса (например, EntryModel) будет:

@class TitleModel;

@interface EntryModel : TinyMappingModel

@property (nonatomic, strong) TitleModel *name;
@property (nonatomic, strong) NSArray *images;

@end

С name все понятно, в массиве images будут храниться объекты типа ImageModel, и, само собой, в имплементации нужно импортить нужные классы (в данном случае ImageModel и TitleModel), можно было бы, конечно, сделать на строках, а потом NSClassFromString, но нет.

Далее, как же работать-то? Когда мы получили из сети данные и каким-то образом преобразовали их в json (например, я часто в небольших проектах использую AFNetworking), следует:

//data - NSDictionary c json
EntryModel *model= [EntryModel mapObjectFromDictionary:data];
//или data - NSArray
EntryModel *modelArray= [EntryModel mapArrayOfObjects:data];

Возможны вариации. Главное передать правильный объект в правильный метод, оно там разберется, ничего сложного.

В двух словах опишу как работает маппер.
Самый важный метод + (instancetype)mapObjectFromDictionary:(NSDictionary *)data, он по сути и делает всю работу — создает будующую модель, итерируется по ключам json, решает во что мапить будем (класс – в зависимости от того, что у нас в keyToClassMappingRules и в какое именное свойство – в зависимости от keyToPropertyNameReplacementRules). Далее, в зависимости от того, что из себя представляют данные для текущего ключа и того, во что будем мапить, есть три пути развития событий:

  • данные массив — будет вызван mapArrayOfObjects классу, в который мапим, и результат установится в соответствующее свойство;
  • данные объект — будет вызван mapObjectFromDictionary (этот же метод, но другого класса, класса из -keyToClassMappingRules), в который мапим, и результат установится в соответствующее свойство;
  • данные «примитив» (NSString, NSNumber etc) и keyToPropertyNameReplacementRules ничего не вернул для данного ключа — KVC установит в нужное свойство без вопросов.

Вот в общем и все. Если подытожить, у нас есть три «сущности» — что мапить, в какой класс мапить или в «примитив», в какое свойство мапить (или если не указано, то в свойство с именем ключа json), и в зависимости от того массив ли значение, которое мапиться или нет, вызывается mapObjectFromDictionary или mapArrayOfObjects, соответсвенно. mapArrayOfObjects никакой работы по сути не делает, создает массив и кладет в него [self mapArrayOfObjects:value] или [mappedArray addObject:[self mapObjectFromDictionary:value], где value — значение, получаемое в ходе итерации по входному массиву. Вот и все.

Спасибо за внимание, интересно ваше мнение об удобстве использования, и, если вы посмотрели исходник, о маппере.
@Disee
карма
0,0
рейтинг 0,0
Реклама помогает поддерживать и развивать наши сервисы

Подробнее
Реклама

Самое читаемое Разработка

Комментарии (12)

  • 0
    Я тут недавно писал про JSON Schema validator. Если пройтись по озвученный минусам, то эта реализация поддерживает mapping; не нужно создавать класс для каждого ключа json'а; полная проверка входных данных, в том числе и типа класса в массивах; небольшой объем исходного кода.

    Но меня на написание своего велосипеда толкнуло полное отсутствие уже готовых решений. Mapping получился в итоге сам собой + все плюшки валидатора
    • 0
      не совсем класс для каждого ключа, только для тех, у которых значение — json класс
      я не очень понял, в вашем JSON Schema validator как происходит маппинг вложеных json объектов? во что они мапятся, тоже веть в классы, для которых нужно создать класс модели?
  • 0
    Ну например, к нам приходит json, у которого по ключику «data» — массив обьектов, которые нужно замапить в наш обьект (назовем его MyClass). Тогда нужно лишь описать схему:
    [[SVType object] properties:@{
                      @"data":[ @[ [MyClass jsonSchema] ] jsonSchema],
                          }];
    

    Mетод -jsonSchema сам создаст схему для нашего класса (все проперти)
    После этого, каждый объект в массиве будет провалидирован в соответствии с этой схемой, и если нужно уже провалидированный json будет инстанциирован в обьекты нашего MyClass. Каждый ключик будет соотнесен со свойством инстанса и присвоен ему.

    Не нужные нам объекты так и остаются NSDictionary и NSArray
    • 0
      если не нужно на каждый объект json создавать по obj-c объекту — оч круто, снимаю шляпу, повнимательнее посмотрю на ваш фреймворк и внутренности его
    • 0
      хотя мне начинает казаться, что я не до конца правильно понял, что значит «создаст схему для нашего класса (все проперти)» — он рантаймом создаст реальные проперти в классе или засетит просто?
      • 0
        Основная идея в том, что создается схема. По сути это описание json'а с указанием того, какие у каких объектов есть свойства, какие у них могут быть значения (если это массив, то какие объекты он может содержать, если число — то какой дианазон оно может принимать, являются ли какие-либо поля строго обязательными и т.д) Вот обзорная статья на хабре . Так вот, метод jsonSchema как раз и создает такую схему для уже существующих классов.
  • 0
    На каждый json объект создавать класс это имхо как-то сурово. Когда мне понадо бился маппер, я нашел github.com/mystcolor/JTObjectMapping и не сожалею)
    • 0
      судя по примеру на гитхабе, у этого маппера та же история, что и у моего — в итоге пришлось создать два класса со всеми нужными проперти — JTSocialNetworkTest и JTUserTest для довольно не сложного json, разве что используя его не пришлось писать свой велик)
      в моем маппере для
      "p_childs" = ( Mary, James )
      тоже не пришлось ничего дополнительно создавать, так что минус с «объект json = объект obj-c» оба мапера решают одинаково
  • 0
    В вашем маппере нравится в первую очередь простота. Но по ходу это единственный его плюс. А теперь о том, что не нравится:

    1) Модель уже не модель
    Теперь у неё есть методы для парсинга объектов и методы для настройки этих методов. И они будут с ней в памяти навсегда.
    Здесь мне больше всё же нравится подход управления моделью снаружи.

    2) Необходимость создавать модели даже для всех вложенных объектов.
    Если данные какого-то json объекта разбиты на несколько групп для простоты их понимания, то я не хочу создавать одну общую модель и несколько вложенных. В этом случае я хочу иметь одну модель и для её свойств указать пути для получения значений. Ну или что-то в этом роде.
    Самое меньшее, что здесь нужно реализовать – добавить возможность хранить вложенный объект в виде словаря без принудительного парсинга. Тогда хотябы костылями можно добиться нужного поведения в сложных случаях.

    Упомянутый выше JTObjectMapping по моему мнению хуже вашего в виду необходимости явно указывать поля для маппинга. А я же в качестве более навороченной альтернативы могу упомянуть github.com/dchohfi/KeyValueObjectMapping. Кроме описанной мной выше необходимости создавать одну модель из нескольких объектов он сможет делать и противоположную вещь – создавать несколько вложенных моделей на основе плоских данных.
    • 0
      абсолютно с вами согласен по всем пунктам, я сам использую свой маппер зачастую в небольших проектах, когда простота принципиальнее всего остального, при этом очень часто сервер пишу либо я сам либо человек, сидящий возле меня, и существует реальная возможность влиять на json) на досуге подумаю над вашими замечаниями
    • 0
      кстати, я внезапно осознал, маппер умеет «Самое меньшее, что здесь нужно реализовать – добавить возможность хранить вложенный объект в виде словаря без принудительного парсинга», просто не нужно указывать в keyToClassMappingRules ключи, которые хотим оставить без парсинга)
      • 0
        Избавится от вложеных объектом можно добавив поддержку keyPath, например:
        - (NSDictionary *)keyToPropertyNameReplacementRules {
            return @{@"image.small":@"smallImage",@"image.big":@"bigImage"};
        }
        

        Таким образом, у нас отпала необходимость в создании модели Image.

Только зарегистрированные пользователи могут оставлять комментарии. Войдите, пожалуйста.