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