NIX ZHU
2020/11/19

Introducing Ducky

Overview

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

  • It can infer JSON Schema, Swift (Codable or AnandaModel), Kotlin, Dart (Null Safety), Go or Proto models.
  • It provides many options for you to customize the model.

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

Ducky is built with SwiftUI, runs on iOS/iPadOS 14, macOS 11 or later.

Overview

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

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

Examples

There is a JSON that represents a post as follow:

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

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

import Foundation

struct Post: Codable {
  struct Creator: Codable {
    let id: Int
    let username: String
    let avatarURL: URL

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

  let id: Int
  let title: String
  let createdAt: Date
  let creator: Creator

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

Note that, by default, the id is inferred as Int, the title is inferred as String, the created_at is inferred as Date and mapped to createdAt, the avatar_url is inferred as URL and mapped to avatarURL.

If you want to change the property's type, for example, infer id as UInt64, you can do that with 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 {
  struct Creator: Codable {
    let id: UInt64
    let username: String
    let avatarURL: URL

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

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

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

Note that both Post's id and Creator's id are inferred as UInt64, if you only needs one be inferred as UInt64, modify the Path to Post.id or creator.id or Post.creator.id, that's how Path works.

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

import Foundation

struct Post: Codable {
  struct Creator: Codable {
    let id: UInt64
    let username: String
    let avatarURL: URL

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

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

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

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

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

And we add another Type Name Maps rule: creator or Post.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 username: String
    let avatarURL: URL

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

    init(id: UInt64, username: String, avatarURL: URL) {
      self.id = id
      self.username = username
      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
  }
}

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 username: String
    let avatarURL: URL

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

    init(id: UInt64, username: String, avatarURL: URL) {
      self.id = id
      self.username = username
      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 username: String
  let avatarURL: URL

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

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

How about Object as Dictionary?

Give a JSON as follow:

{
  "countries": {
    "china": {
      "population": "1,412,600,000",
      "market": {
        "value": 8964
      }
    },
    "usa": {
      "population": "332,580,125",
      "freedomOfSpeech": true,
      "market": {
        "value": 100000,
        "companies": [
          "Apple",
          "Microsoft",
          "Amazon",
          "Alphabet"
        ]
      }
    }
  }
}

Set Model Name to World, by default, the countries will be inferred as a struct. But with the Object As Dictionary option, add a rule: countries for the Path, it will be inferred as a Dictionary.

import Foundation

struct World: Codable {
  struct China: Codable {
    struct Market: Codable {
      let value: Int
      let companies: [String]?
    }

    let population: String
    let market: Market
    let freedomOfSpeech: Bool?
  }

  let countries: [String: China]
}

Note that, the Dictionary's value type is China (It merged with china and usa) , we can modify it with Type Name Maps, add a rule: china for the Path, Country for the Name, then we have:

import Foundation

struct World: Codable {
  struct Country: Codable {
    struct Market: Codable {
      let value: Int
      let companies: [String]?
    }

    let population: String
    let market: Market
    let freedomOfSpeech: Bool?
  }

  let countries: [String: Country]
}

Looks good, right?

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]?
}

Faster JSON decoding with Ananda

Besides Codable, you can also use AnandaModel to parse JSON in Swift. Suppose we have JSON like this:

{
  "id": "chatcmpl-72yPTB5AukZUhXAEmkQy5BYqZDvLX",
  "object": "chat.completion",
  "created": 1680943227,
  "model": "gpt-3.5-turbo-0301",
  "usage": {
    "prompt_tokens": 16,
    "completion_tokens": 9,
    "total_tokens": 25
  },
  "choices": [
    {
      "message": {
        "role": "assistant",
        "content": "Hello! How can I assist you today?"
      },
      "finish_reason": "stop",
      "index": 0
    }
  ]
}

After you set Protocol Type to AnandaModel, you will get:

import Foundation
import Ananda

struct ChatCompletionOutput: AnandaModel {
  struct Usage: AnandaModel {
    let promptTokens: Int
    let completionTokens: Int
    let totalTokens: Int

    init(json: AnandaJSON) {
      promptTokens = json.prompt_tokens.int()
      completionTokens = json.completion_tokens.int()
      totalTokens = json.total_tokens.int()
    }
  }

  struct Choice: AnandaModel {
    struct Message: AnandaModel {
      let role: String
      let content: String

      init(json: AnandaJSON) {
        role = json.role.string()
        content = json.content.string()
      }
    }

    let message: Message
    let finishReason: String
    let index: Int

    init(json: AnandaJSON) {
      message = .init(json: json.message)
      finishReason = json.finish_reason.string()
      index = json.index.int()
    }
  }

  let id: String
  let object: String
  let created: Date
  let model: String
  let usage: Usage
  let choices: [Choice]

  init(json: AnandaJSON) {
    id = json.id.string()
    object = json.object.string()
    created = json.created.date()
    model = json.model.string()
    usage = .init(json: json.usage)
    choices = json.choices.array().map { .init(json: $0) }
  }
}

Ananda is a mini framework I build for faster and safer JSON decoding based on yyjson.

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. :]