Introducing Ducky

2020-11-19

Overview

Ducky is a document-based app that helps you infer models from JSON to save your time.

In short, it's a model type generator for JSON.

Ducky is built using SwiftUI, run on iOS/iPadOS 14 or macOS 11 Big Sur.

Ducky Overview

If you work with RESTful APIs, Ducky should save you a lot of time building the model layer.

Examples

There is a JSON that represents a post as follow.

{
  "id": 1,
  "title": "Test",
  "created_at": "2020-11-18T18:25:43.511Z"
}

If you choose Swift as Output Type, set Model Name to Post, Ducky will infer it as follow.

import Foundation

struct Post: Codable {
  let id: Int
  let title: String
  let createdAt: Date

  private enum CodingKeys: String, CodingKey {
    case id
    case title
    case createdAt = "created_at"
  }
}

Note that, by default, the id is inferred as Int, the title is inferred as String, the created_at is inferred as Date. Also note that created_at is mapped to createdAt.

If you want to change the property's type, for example, infer id as UInt64, you can do that by Type Name Maps. Just add a rule: id for the Path, UInt64 for the Name. Ducky will infer it as follow.

import Foundation

struct Post: Codable {
  let id: UInt64
  let title: String
  let createdAt: Date

  private enum CodingKeys: String, CodingKey {
    case id
    case title
    case createdAt = "created_at"
  }
}

If you check Needs Initializer, Ducky will infer it as follow.

import Foundation

struct Post: Codable {
  let id: UInt64
  let title: String
  let createdAt: Date

  private enum CodingKeys: String, CodingKey {
    case id
    case title
    case createdAt = "created_at"
  }

  init(id: UInt64, title: String, createdAt: Date) {
    self.id = id
    self.title = title
    self.createdAt = createdAt
  }
}

How about nested JSON?

We give the post a creator as follow.

{
  "id": 1,
  "title": "Test",
  "created_at": "2020-11-18T18:25:43.511Z",
  "creator": {
    "id": 42,
    "name": "nixzhu",
    "avatar_url": "https://avatar.com/nixzhu.png"
  }
}

And we add another Type Name Maps rule: creator for the Path, User for the Name. Ducky will infer it as follow.

import Foundation

struct Post: Codable {
  struct User: Codable {
    let id: UInt64
    let name: String
    let avatarURL: URL

    private enum CodingKeys: String, CodingKey {
      case id
      case name
      case avatarURL = "avatar_url"
    }

    init(id: UInt64, name: String, avatarURL: URL) {
      self.id = id
      self.name = name
      self.avatarURL = avatarURL
    }
  }

  let id: UInt64
  let title: String
  let createdAt: Date
  let creator: User

  private enum CodingKeys: String, CodingKey {
    case id
    case title
    case createdAt = "created_at"
    case creator
  }

  init(id: UInt64, title: String, createdAt: Date, creator: User) {
    self.id = id
    self.title = title
    self.createdAt = createdAt
    self.creator = creator
  }
}

Note that we have a nested User inside the Post. Also Note that the avatar_url is inferred as URL, and is mapped to avatarURL.

If you don't like the Nested structure. You can change the Structure Style to Extended or Flat.

If you choose Extended, Ducky will infer it as follow.

import Foundation

struct Post: Codable {
  let id: UInt64
  let title: String
  let createdAt: Date
  let creator: User

  private enum CodingKeys: String, CodingKey {
    case id
    case title
    case createdAt = "created_at"
    case creator
  }

  init(id: UInt64, title: String, createdAt: Date, creator: User) {
    self.id = id
    self.title = title
    self.createdAt = createdAt
    self.creator = creator
  }
}

extension Post {
  struct User: Codable {
    let id: UInt64
    let name: String
    let avatarURL: URL

    private enum CodingKeys: String, CodingKey {
      case id
      case name
      case avatarURL = "avatar_url"
    }

    init(id: UInt64, name: String, avatarURL: URL) {
      self.id = id
      self.name = name
      self.avatarURL = avatarURL
    }
  }
}

If you choose Flat, Ducky will infer it as follow.

import Foundation

struct Post: Codable {
  let id: UInt64
  let title: String
  let createdAt: Date
  let creator: User

  private enum CodingKeys: String, CodingKey {
    case id
    case title
    case createdAt = "created_at"
    case creator
  }

  init(id: UInt64, title: String, createdAt: Date, creator: User) {
    self.id = id
    self.title = title
    self.createdAt = createdAt
    self.creator = creator
  }
}

struct User: Codable {
  let id: UInt64
  let name: String
  let avatarURL: URL

  private enum CodingKeys: String, CodingKey {
    case id
    case name
    case avatarURL = "avatar_url"
  }

  init(id: UInt64, name: String, avatarURL: URL) {
    self.id = id
    self.name = name
    self.avatarURL = avatarURL
  }
}

How about array in JSON?

There is a JSON that represents a library's books as follow.

{
  "books": [
    {
      "id": 1,
      "language": "C",
      "author": "Dennis Ritchie"
    },
    {
      "id": 2,
      "language": "C++",
      "author": " Bjarne Stroustrup"
    }
  ]
}

If we set Model Name to Library. Add a Type Name Maps rule: books for the Path, Book for the Name. And add a Property Enum Maps rule: language for the Path, and three cases: c|C, cpp|C++, swift|Swift. Ducky will infer it as follow.

import Foundation

struct Library: Codable {
  struct Book: Codable {
    enum Language: String, Codable {
      case c = "C"
      case cpp = "C++"
      case swift = "Swift"
    }

    let id: Int
    let language: Language
    let author: String
  }

  let books: [Book]
}

If you check All Properties Optional, Ducky will infer it as follow.

import Foundation

struct Library: Codable {
  struct Book: Codable {
    enum Language: String, Codable {
      case c = "C"
      case cpp = "C++"
      case swift = "Swift"
    }

    let id: Int?
    let language: Language?
    let author: String?
  }

  let books: [Book]?
}

There are other options, just try it.

How about Kotlin, Dart and Go models?

Give a JSON as follow.

{
  "id": 0,
  "title": "Hello World",
  "body": "I'm Ducky, help you infer models from JSON.",
  "outputTypes": [
    "Swift",
    "Kotlin",
    "Dart",
    "Go"
  ],
  "developer": {
    "username": "nixzhu",
    "email": "zhuhongxu@gmail.com"
  }
}

If you choose Kotlin as Output Type, set Model Name to Hello, Ducky will infer it as follow.

package ducky

import kotlinx.serialization.*
import kotlinx.serialization.json.*
import kotlinx.serialization.internal.*

@Serializable
data class Hello (
  val id: Long,
  val title: String,
  val body: String,
  val outputTypes: List<String>,
  val developer: Developer
) {
  @Serializable
  data class Developer (
    val username: String,
    val email: String
  )
}

If you change the Structure Style to Flat, Ducky will infer it as follow.

package ducky

import kotlinx.serialization.*
import kotlinx.serialization.json.*
import kotlinx.serialization.internal.*

@Serializable
data class Hello (
  val id: Long,
  val title: String,
  val body: String,
  val outputTypes: List<String>,
  val developer: Developer
)

@Serializable
data class Developer (
  val username: String,
  val email: String
)

If you choose Dart as Output Type, Ducky will infer it as follow.

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

class Hello {
  int id;
  String title;
  String body;
  List<String> outputTypes;
  Developer developer;

  Hello({
    this.id,
    this.title,
    this.body,
    this.outputTypes,
    this.developer,
  });

  factory Hello.fromJSON(Map<String, dynamic> json) => Hello(
    id: json["id"],
    title: json["title"],
    body: json["body"],
    outputTypes: List<String>.from(json["outputTypes"].map((x) => x)),
    developer: Developer.fromJSON(json["developer"]),
  );

  Map<String, dynamic> toJSON() => {
    "id": id,
    "title": title,
    "body": body,
    "outputTypes": List<dynamic>.from(outputTypes.map((x) => x)),
    "developer": developer.toJSON(),
  };
}

class Developer {
  String username;
  String email;

  Developer({
    this.username,
    this.email,
  });

  factory Developer.fromJSON(Map<String, dynamic> json) => Developer(
    username: json["username"],
    email: json["email"],
  );

  Map<String, dynamic> toJSON() => {
    "username": username,
    "email": email,
  };
}

If you choose Go as Output Type, Ducky will infer it as follow.

package main

type Hello struct {
  ID int `json:"id"`
  Title string `json:"title"`
  Body string `json:"body"`
  OutputTypes []string `json:"outputTypes"`
  Developer struct {
    Username string `json:"username"`
    Email string `json:"email"`
  } `json:"developer"`
}

If you change the Structure Style to Flat, Ducky will infer it as follow.

package main

type Hello struct {
  ID int `json:"id"`
  Title string `json:"title"`
  Body string `json:"body"`
  OutputTypes []string `json:"outputTypes"`
  Developer Developer `json:"developer"`
}

type Developer struct {
  Username string `json:"username"`
  Email string `json:"email"`
}

Also, there are other options, just try it.

Finally

Get it on the App Store. If the link doesn't work, try searching for ducky model editor in the App Store.

If you have any questions or suggestions, please to contact me via Email.

Hope you like this Ducky. :]