DEV Community

Cover image for Dart/Flutter da Json Ayrıştırma 💫 🌌 ✨
Gülsen Keskin
Gülsen Keskin

Posted on • Updated on

Dart/Flutter da Json Ayrıştırma 💫 🌌 ✨

Not: Bu yazı Andrea Bizzotto'nun makalesinin Türkçe çevirisi niteliğini taşır. Orjinal içeriğe linkten ulaşabilirsiniz.

JSON ayrıştırma için ne kadar JSON verisi işlemeniz gerektiğine bağlı olarak iki seçeneğiniz vardır:

  1. Tüm JSON ayrıştırma kodunu manuel olarak yazın
  2. Kod oluşturma ile süreci otomatikleştirin

Bu kılavuz, aşağıdakiler dahil olmak üzere JSON'un Dart koduna manuel olarak nasıl ayrıştırılacağına odaklanacaktır :

☼ JSON encoding (kodlama) ve decoding (kod çözme)
☼ Type-safe (tip güvenli) model sınıfları tanımlama
☼ Factory cunstructor kullanarak JSON'u Dart koduna ayrıştırma
☼ Nullable/optional (null yapılabilir/isteğe bağlı) değerlerle çalışmak
☼ Data validation (veri doğrulama)
☼ JSON serializing
☼ Complex/nested (karmaşık/iç içe) JSON verilerini ayrıştırma
☼ deep_pick paketinin kullanımı

Bu makalenin sonunda, sağlam JSON ayrıştırma ve doğrulama koduyla model sınıflarının nasıl yazılacağını öğreneceksiniz.

Ve bir sonraki makalede , tüm ayrıştırma kodunu elle yazmak zorunda kalmamak için kod oluşturma araçlarıyla (code generation tools) JSON ayrıştırma hakkında bilgi edineceksiniz.

JSON Kodlama ve Kod Çözme (Encoding and Decoding JSON)

Ağ üzerinden bir JSON yanıtı gönderildiğinde, yükün tamamı bir dize olarak kodlanır .

Ancak Flutter uygulamalarımızın içinde, verileri bir dizeden manuel olarak çıkarmak istemiyoruz:

final json = '{ "name": "Pizza da Mario", "cuisine": "Italian", "reviews": [{"score": 4.5,"review": "The pizza was amazing!"},{"score": 5.0,"review": "Very friendly staff, excellent service!"}]}';
Enter fullscreen mode Exit fullscreen mode

Bunun yerine JSON'un kodunu çözerek içeriği okuyabiliyoruz .

JSON verilerini ağ üzerinden göndermek için önce kodlanması (encoded) veya serileştirilmesi (serialized) gerekir . Kodlama , bir veri yapısını (data structure) bir string'e dönüştürme işlemidir . Ters işlem, decoding veya deserialization (seri durumdan çıkarma) olarak adlandırılır . Dize olarak bir JSON yükü aldığınızda, kullanmadan önce kodunu çözmeniz (decode) veya seri durumdan çıkarmanız (deserialize) gerekir.

Dart ile JSON kodunu çözme:dönüştürme (Decoding JSON with dart:convert)

Basit olması için, bu küçük JSON yükünü ele alalım:

//bu, ağdan aldığımız bazı yanıt verilerini temsil eder, örneğin:
// final response = await http.get(uri);
// final jsonData = response.body
final jsonData = '{ "name": "Pizza da Mario", "cuisine": "Italian" }';
Enter fullscreen mode Exit fullscreen mode

İçindeki anahtarları ve değerleri okumak için önce dart:convert paketini kullanarak kodunu çözmemiz gerekiyor

// 1. import dart:convert
import 'dart:convert';
// bu, ağdan aldığımız bazı yanıt verilerini temsil eder
final jsonData = '{ "name": "Pizza da Mario", "cuisine": "Italian" }';
// 2. json kodunu çöz
final parsedJson = jsonDecode(jsonData);
// 3. türü ve değeri yazdır
print('${parsedJson.runtimeType} : $parsedJson');
Enter fullscreen mode Exit fullscreen mode

Bu kodu çalıştırırsak şu çıktıyı alırız:

_InternalLinkedHashMap<String, dynamic> : {name: Pizza da Mario, cuisine: Italian}
Enter fullscreen mode Exit fullscreen mode

Pratikte, sonuç Map türü ile aynıdır.

_InternalLinkedHashMap , sırayla Map'i uygulayan LinkedHashMap'in özel bir uygulamasıdır.

Bu nedenle, anahtarlar String ve değerler dynamic türündedir . Bu mantıklıdır çünkü her JSON değeri primitive type (ilkel bir tür => boolean/number/string) veya bir koleksiyon (list veya map) olabilir.

Aslında, jsonDecode(), içinde ne olduğuna bakılmaksızın herhangi bir geçerli JSON yükü üzerinde çalışan genel bir yöntemdir. Tek yaptığı, kodunu çözmek ve dinamik bir değer döndürmek.

Ancak Dart'ta dinamik değerlerle çalışırsak, strong type-safety (güçlü tip güvenliğinin) tüm avantajlarını kaybederiz. Çok daha iyi bir yaklaşım, duruma göre yanıt verilerimizi temsil eden bazı özel model sınıfları tanımlamaktır.

Dart statik olarak yazılmış bir dil olduğundan, JSON verilerini gerçek dünya nesnelerini (yemek tarifi, çalışan vb.) temsil eden model sınıflarına dönüştürmek ve tür sisteminden (type system) en iyi şekilde yararlanmak önemlidir.

Öyleyse bunun nasıl yapılacağını görelim.

JSON'u bir Dart modeli sınıfına ayrıştırma

Bu basit JSON verildiğinde:

{
  "name": "Pizza da Mario",
  "cuisine": "Italian"
}
Enter fullscreen mode Exit fullscreen mode

Onu temsil edecek bir Restaurant sınıfı yazabiliriz:

class Restaurant {
  Restaurant({required this.name, required this.cuisine});
  final String name;
  final String cuisine;
}
Enter fullscreen mode Exit fullscreen mode

Sonuç olarak, verileri şöyle okumak yerine:

parsedJson['name']; // dynamic
parsedJson['cuisine']; // dynamic
Enter fullscreen mode Exit fullscreen mode

Böyle okuyabiliriz:

restaurant.name; //geçersiz kılınamaz, değişmez bir string olması garanti edilir
restaurant.cuisine; // geçersiz kılınamaz, değişmez bir stringolması garanti edilir
Enter fullscreen mode Exit fullscreen mode

Bu çok daha temizdir ve derleme zamanı güvenliği ( compile-time safety) elde etmek ve yazım hatalarını ve diğer hatalardan kaçınmak için tür sisteminden yararlanabiliriz.

Ancak, parsedJson'umuzu bir Restaurant nesnesine nasıl dönüştüreceğimizi henüz belirlemedik!

Factory Constructor Ekleme

Bunu halletmek için bir factory constructor tanımlayalım:

factory Restaurant.fromJson(Map<String, dynamic> data) {
  // note the explicit cast to String
  // this is required if robust lint rules are enabled
  final name = data['name'] as String;
  final cuisine = data['cuisine'] as String;
  return Restaurant(name: name, cuisine: cuisine);
}
Enter fullscreen mode Exit fullscreen mode

Bir factory constructor, sonucu döndürmeden önce bazı işler yapmamıza (değişkenler oluşturma, bazı doğrulamalar gerçekleştirme) izin verdiği için JSON ayrıştırması (parsing) için iyi bir seçimdir . Bu, normal (generative-üretken) constructor'larla mümkün değildir.

Constructor'ı şu şekilde kullanabiliriz:

// type: String
final jsonData = '{ "name": "Pizza da Mario", "cuisine": "Italian" }';
// type: dynamic (runtime type: _InternalLinkedHashMap<String, dynamic>)
final parsedJson = jsonDecode(jsonData);
// type: Restaurant
final restaurant = Restaurant.fromJson(parsedJson);
Enter fullscreen mode Exit fullscreen mode

Çok daha iyi. Artık kodumuzun geri kalanı Restaurant class'ını kullanabilir ve Dart'ta güçlü tip güvenliğinin (type-safety) tüm avantajlarını elde edebilir.

Bazen , belirli bir key/value çiftine sahip olan veya olmayan bazı JSON'ları ayrıştırmamız gerekir .

Örneğin, bir restoranın ilk ne zaman açıldığını bize bildiren isteğe bağlı bir alanımız olduğunu varsayalım:

{
  "name": "Ezo Sushi",
  "cuisine": "Japanese",
  "year_opened": 1990
}
Enter fullscreen mode Exit fullscreen mode

Year_opened alanı isteğe bağlıysa (optional), onu model sınıfımızda boş bir değişkenle (nullable variable) temsil edebiliriz.

İşte Restaurant sınıfı için güncellenmiş bir uygulama:

class Restaurant {
  Restaurant({required this.name, required this.cuisine, this.yearOpened});
  final String name; // non-nullable
  final String cuisine; // non-nullable
  final int? yearOpened; // nullable

  factory Restaurant.fromJson(Map<String, dynamic> data) {
    final name = data['name'] as String; // cast as non-nullable String
    final cuisine = data['cuisine'] as String; // cast as non-nullable String
    final yearOpened = data['year_opened'] as int?; // cast as nullable int
    return Restaurant(name: name, cuisine: cuisine, yearOpened: yearOpened);
  }
}
Enter fullscreen mode Exit fullscreen mode

Genel bir kural olarak, isteğe bağlı JSON değerlerini (optional JSON values) null yapılabilir Dart özelliklerine (nullable Dart properties) eşlemeliyiz . Alternatif olarak, bu örnekte olduğu gibi, null yapılamayan Dart özelliklerini (non-nullable Dart properties) mantıklı bir varsayılan değerle kullanabiliriz:

// note: all the previous properties have been omitted for simplicity
class Restaurant {
  Restaurant({
    // 1. required
    required this.hasIndoorSeating,
  });
  // 2. *non-nullable*
  final bool hasIndoorSeating;

  factory Restaurant.fromJson(Map<String, dynamic> data) {
    // 3. cast as *nullable* bool
    final hasIndoorSeating = data['has_indoor_seating'] as bool?;
    return Restaurant(
      // 4. varsayılan bir dddeğer atamak için ?? operatörünü kullanmak
      hasIndoorSeating: hasIndoorSeating ?? true,
    );
  }
}
Enter fullscreen mode Exit fullscreen mode

Bu durumda, varsayılan bir değer sağlamak için boş birleştirme operatörünü (null-coalescing operator) (??) nasıl kullandığımıza dikkat edin.

Veri Doğrulama

Factory constructor kullanmanın bir yararı, gerekirse bazı ek doğrulamalar yapabilmemizdir.

Örneğin, gerekli bir değer eksikse UnsupportedError veren bir savunma kodu yazabiliriz.

factory Restaurant.fromJson(Map<String, dynamic> data) {
  // casting as a nullable String so we can do an explicit null check
  final name = data['name'] as String?;
  if (name == null) {
    throw UnsupportedError('Invalid data: $data -> "name" is missing');
  }
  final cuisine = data['cuisine'] as String?;
  if (cuisine == null) {
    throw UnsupportedError('Invalid data: $data -> "cuisine" is missing');
  }
  final yearOpened = data['year_opened'] as int?;
  // Yukarıdaki if ifadeleri sayesinde, burada name ve cuisine değerlerinin  boş olmayacağı garanti edilir.
  return Restaurant(name: name, cuisine: cuisine, yearOpened: yearOpened);
}
Enter fullscreen mode Exit fullscreen mode

Genel olarak, her bir değer için çalışmak API tüketicisi olarak bizim işimizdir:

türü (String, int, vb.)
isteğe bağlıysa veya değilse (nullable vs non-nullable)
hangi değer aralığına izin verilirse

Bu, JSON ayrıştırma kodumuzu daha sağlam hale getirecektir. Ve tüm doğrulamalar önceden yapıldığından , widget sınıflarımızda geçersiz verilerle uğraşmak zorunda kalmayacağız .

toJson() ile JSON Serileştirme

JSON'u ayrıştırmak yararlıdır, ancak bazen bir model nesnesini JSON'a geri dönüştürmek ve ağ üzerinden göndermek isteriz.

Bunu yapmak için Restaurant sınıfımız için bir toJson() yöntemi tanımlayabiliriz:

// note the return type
Map<String, dynamic> toJson() {
  // return a map literal with all the non-null key-value pairs
  return {
    'name': name,
    'cuisine': cuisine,
    if (yearOpened != null) 'year_opened': yearOpened,
  };
}
Enter fullscreen mode Exit fullscreen mode

Ve bunu şöyle kullanabiliriz:

// bir Restoran nesnesi verildi
final restaurant = Restaurant(name: "Patatas Bravas", cuisine: "Spanish");
// convert it to map
final jsonMap = restaurant.toJson();
// onu bir JSON dizgisine kodla
final encodedJson = jsonEncode(jsonMap);
// sonra herhangi bir ağ paketiyle istek gövdesi olarak gönder
Enter fullscreen mode Exit fullscreen mode

İç İçe JSON'ı Ayrıştırma: List of Map

Artık JSON ayrıştırma ve doğrulamanın temellerini anladığımıza göre, ilk örneğimize geri dönelim ve nasıl ayrıştırılacağını görelim:

{
  "name": "Pizza da Mario",
  "cuisine": "Italian",
  "reviews": [
    {
      "score": 4.5,
      "review": "The pizza was amazing!"
    },
    {
      "score": 5.0,
      "review": "Very friendly staff, excellent service!"
    }
  ]
}
Enter fullscreen mode Exit fullscreen mode

Model sınıflarını ve tip güvenliğini (type-safety) sonuna kadar kullanmak istiyoruz, bu yüzden bir Review sınıfı tanımlayalım:

class Review {
  Review({required this.score, this.review});
  // non-nullable - puan alanının her zaman mevcut olduğu varsayılır
  final double score;
  // nullable - inceleme alanının isteğe bağlı olduğunu varsayarsak
  final String? review;

  factory Review.fromJson(Map<String, dynamic> data) {
    final score = data['score'] as double;
    final review = data['review'] as String?;
    return Review(score: score, review: review);
  }

  Map<String, dynamic> toJson() {
    return {
      'score': score,
      // burada, null değerleri hesaba katmak için collection-if kullanıyoruz
      if (review != null) 'review': review,
    };
  }
}
Enter fullscreen mode Exit fullscreen mode

Ardından, bir reviews listesi eklemek için Restoran class'ını güncelleyebiliriz:

class Restaurant {
  Restaurant({
    required this.name,
    required this.cuisine,
    this.yearOpened,
    required this.reviews,
  });
  final String name;
  final String cuisine;
  final int? yearOpened;
  final List<Review> reviews;
}
Enter fullscreen mode Exit fullscreen mode

Ayrıca factory cunstructor'ını da güncelleyebiliriz:

factory Restaurant.fromJson(Map<String, dynamic> data) {
  final name = data['name'] as String;
  final cuisine = data['cuisine'] as String;
  final yearOpened = data['year_opened'] as int?;
  // reviews eksik olabileceğinden null yapılabilir bir listeye dönüştürün
  final reviewsData = data['reviews'] as List<dynamic>?;
  // reviews eksik değilse
  final reviews = reviewsData != null
      // her review'i  bir Review object'e eşleyin
      ? reviewsData.map((reviewData) => Review.fromJson(reviewData))
        // map() yinelenebilir bir değer döndürür böylece onu listeye çevirebiliriz
        .toList()
      // geri dönüş değeri olarak boş bir liste kullanın
      : <Review>[];
  // tüm argümanları geçen sonucu döndür
  return Restaurant(
    name: name,
    cuisine: cuisine,
    yearOpened: yearOpened,
    reviews: reviews,
  );
}
Enter fullscreen mode Exit fullscreen mode

• reviews eksik olabilir, bu nedenle nullable bir Listeye yayınlıyoruz.
• listedeki değerlerin herhangi bir türü olabilir, bu nedenle List kullanıyoruz.
• Review.fromJson() kullanarak her dinamik değeri bir Review nesnesine dönüştürmek için .map() operatörünü kullanırız.
• reviews eksikse, yedek olarak boş bir liste ([]) kullanırız.

Bu özel uygulama, neyin boş olup olmayacağı, hangi yedek değerlerin kullanılacağı vb. hakkında bazı varsayımlarda bulunur. Kullanım durumunuz için en uygun ayrıştırma kodunu yazmanız gerekir.

İç İçe (Nested) Modelleri Serileştirme

Son adım olarak, bir Restoranı tekrar Map'e dönüştürmek için toJson() yöntemini burada bulabilirsiniz:

Map<String, dynamic> toJson() {
  return {
    'name': name,
    'cuisine': cuisine,
    if (yearOpened != null) 'year_opened': yearOpened,
    'reviews': reviews.map((review) => review.toJson()).toList(),
  };
}
Enter fullscreen mode Exit fullscreen mode

Tüm iç içe değerleri de serileştirmemiz gerektiğinden (yalnızca Restaurant sınıfının kendisini değil) List öğesini nasıl List>'e dönüştürdüğümüze dikkat edin.

Yukarıdaki kod ile bir Restaurant nesnesi oluşturup onu tekrar kodlanıp yazdırılabilen veya ağ üzerinden gönderilebilen bir map'e dönüştürebiliriz:

final restaurant = Restaurant(
  name: 'Pizza da Mario',
  cuisine: 'Italian',
  reviews: [
    Review(score: 4.5, review: 'The pizza was amazing!'),
    Review(score: 5.0, review: 'Very friendly staff, excellent service!'),
  ],
);
final encoded = jsonEncode(restaurant.toJson());
print(encoded);
// output: {"name":"Pizza da Mario","cuisine":"Italian","reviews":[{"score":4.5,"review":"The pizza was amazing!"},{"score":5.0,"review":"Very friendly staff, excellent service!"}]}
Enter fullscreen mode Exit fullscreen mode

Derin Değerler (Deep Values) Seçmek

Tüm bir JSON belgesini type-safe model sınıflarına ayrıştırmak çok yaygın bir kullanım durumudur.

Ancak bazen derinden iç içe (deeply nested) olabilecek bazı belirli değerleri okumak isteriz.

Örnek JSON'umuzu bir kez daha ele alalım:

{
  "name": "Pizza da Mario",
  "cuisine": "Italian",
  "reviews": [
    {
      "score": 4.5,
      "review": "The pizza was amazing!" 
    },
    {
      "score": 5.0,
      "review": "Very friendly staff, excellent service!"
    }
  ]
}
Enter fullscreen mode Exit fullscreen mode

reviews listesindeki ilk score değerini almak isteseydik şöyle yazardık:

final decodedJson = jsonDecode(jsonData); // dynamic
final score = decodedJson['reviews'][0]['score'] as double;
Enter fullscreen mode Exit fullscreen mode

Bu geçerli Dart kodudur çünkü decodedJson değişkeni dinamiktir ve onunla birlikte indis operatörünü kullanabiliriz ([]).

Ancak yukarıdaki kod ne null safe ne de type safe ve ayrıştırılan değeri açıkça istediğimiz türe (double) çevirmemiz gerekiyor.

Bunu nasıl iyileştirebiliriz?

deep_pick Paketi

Deep_pick paketi, tür açısından type-safe API ile JSON parsing'i basitleştirir.

import 'dart:convert';
import 'package:deep_pick/deep_pick.dart';

final decodedJson = jsonDecode(jsonData); // dynamic
final score = pick(decodedJson, 'reviews', 0, 'score').asDoubleOrThrow();
Enter fullscreen mode Exit fullscreen mode

deep_pick, ilkel türleri (primitive types), listeleri, mapleri, DateTime nesnelerini ve daha fazlasını ayrıştırmak için kullanabileceğimiz çeşitli esnek API'ler sunar.

toString() yöntemi ekleme

Model sınıflarıyla çalışırken, konsola kolayca yazdırılabilmeleri için bir toString() yöntemi sağlamak çok yararlıdır.

Zaten bir toJson() yöntemimiz olduğundan, onu şu şekilde kullanabiliriz:

@override
String toString() => toJson().toString();
Enter fullscreen mode Exit fullscreen mode

Sonuç olarak, restoranımızı doğrudan şu şekilde yazdırabiliriz:

print(restaurant);
// output: {name: Pizza da Mario, cuisine: Italian, reviews: [{score: 4.5, review: The pizza was amazing!}, {score: 5.0, review: Very friendly staff, excellent service!}]}
Enter fullscreen mode Exit fullscreen mode

Performans Hakkında Not

Küçük JSON belgelerini ayrıştırdığınızda, uygulamanızın yanıt vermeye devam etmesi ve performans sorunları yaşamaması muhtemeldir.

Ancak çok büyük JSON belgelerinin ayrıştırılması, arka planda en iyi şekilde ayrı bir Dart isolate üzerinde yapılan pahalı hesaplamalara neden olabilir . Resmi belgelerin bu konuda iyi bir kılavuzu var:
JSON'u arka planda ayrıştırın

Çözüm

JSON serileştirme çok sıradan bir iştir. Ancak uygulamalarımızın doğru çalışmasını istiyorsak, bunu doğru yapmamız ve ayrıntılara dikkat etmemiz çok önemlidir:

• JSON verilerini seri hale getirmek için 'dart:convert' öğesinden jsonEncode() ve jsonDecode() kullanın

• Uygulamanızdaki tüm alana özgü (domain-specific) JSON nesneleri için fromJson() ve toJson() ile model sınıfları oluşturun

• Ayrıştırma kodunu daha sağlam hale getirmek için fromJson() içine explicit casts, validation ve boş denetimler(null checks) ekleyin

• İç içe (nested) JSON verileri (list of maps) için fromJson() ve toJson() yöntemlerini uygulayın

• JSON'u tür açısından güvenli(type-safe) bir şekilde ayrıştırmak (parse) için deep_pick paketini kullanmayı düşünün

Farklı model sınıfınız varsa veya her sınıfın birçok özelliği varsa, tüm ayrıştırma kodunu elle yazmak zaman alıcı ve hataya açık hale gelir.

Bu gibi durumlarda kod oluşturma(code generation) çok daha iyi bir seçenektir.

resource

Top comments (0)