Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Docs #1221

Draft
wants to merge 6 commits into
base: master
Choose a base branch
from
Draft

Docs #1221

Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
Add the Getting Started tutorial
plokhotnyuk committed Nov 22, 2024

Verified

This commit was created on GitHub.com and signed with GitHub’s verified signature.
commit 1b49510e341afa969d9a108235c45ab6fd4c8159
210 changes: 205 additions & 5 deletions docs/tutorials/1-getting-started.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,207 @@
Goal: show the power of auto-derivation
# Getting started
Let's start our adventure with jsoniter-scala from parsing and serialization of some complex data structure of nested
collections and case classes.

Example:
1.
Nested case classes with a couple of collections (`Seq` and `Map`)
## Prerequisites
In all tutorials we will use [scala-cli](https://scala-cli.virtuslab.org) for running Scala scripts, so you'll need to install it upfront.

Challenge: find concrete (non-abstract) collection from standard Scala library that is not supported yet by auto-derivation
You can use any text editor to copy and paste provided code snippets. Please, also, check Scala CLI Cookbooks
documentation if you use [VSCode](https://scala-cli.virtuslab.org/docs/cookbooks/ide/vscode), [Intellij IDEA](https://scala-cli.virtuslab.org/docs/cookbooks/ide/intellij), or [emacs](https://scala-cli.virtuslab.org/docs/cookbooks/ide/emacs) editors.

Support of Scala CLI by IDEs is improving over time, so that [IntelliJ IDEA](https://www.jetbrains.com/idea/) with the latest version of Scala
plugin you can try its [improved support for Scala CLI projects](https://blog.jetbrains.com/scala/2024/11/13/intellij-scala-plugin-2024-3-is-out/#scala-cli).

Latest versions of jsoniter-scala libraries require JDK 11 or above. You can use any of its distributions.

In all tutorials we will start from a file with the following dependencies and imports:

```scala
//> using scala 3
//> using dep "com.github.plokhotnyuk.jsoniter-scala::jsoniter-scala-core::2.31.3"
//> using compileOnly.dep "com.github.plokhotnyuk.jsoniter-scala::jsoniter-scala-macros::2.31.3"

import com.github.plokhotnyuk.jsoniter_scala.core._
import com.github.plokhotnyuk.jsoniter_scala.macros._
```

Please just copy and paste code snippets from the subsequent steps and run by pressing some `Run` button/keystroke in
your IDE or run it using `scala-cli <file_name>` command from the terminal.

Make sure to preserve indentation when copying and pasting because we will use indenting syntax of Scala 3.

## Definition of a complex data structure

Let's imagine that we need to generate a JSON representation of some report for a US shop.

Below is a definition of its data structures and instantiation of some sample data:

```scala
enum Category extends Enum[Category]:
case Electronics, Fashion, HomeGoods

case class Product(id: Long, name: String, category: Category, price: BigDecimal, description: String)

enum OrderStatus extends Enum[OrderStatus]:
case Pending, Shipped, Delivered, Cancelled

case class OrderItem(product: Product, quantity: Int)

case class Order(id: Long, customer: Customer, items: List[OrderItem], status: OrderStatus)

case class Customer(id: Long, name: String, email: String, address: Address)

case class Address(street: String, city: String, state: String, zip: String)

enum PaymentType extends Enum[PaymentType]:
case CreditCard, PayPal

case class PaymentMethod(`type`: PaymentType, details: Map[String, String]/*e.g. card number, expiration date*/)

case class Payment(method: PaymentMethod, amount: BigDecimal, timestamp: java.time.Instant)

case class OrderPayment(order: Order, payment: Payment)

val product1 = Product(
id = 1L,
name = "Apple iPhone 16",
category = Category.Electronics,
price = BigDecimal(999.99),
description = "A high-end smartphone with advanced camera and AI capabilities"
)
val product2 = Product(
id = 2L,
name = "Nike Air Max 270",
category = Category.Fashion,
price = BigDecimal(129.99),
description = "A stylish and comfortable sneaker with a full-length air unit"
)
val product3 = Product(
id = 3L,
name = "KitchenAid Stand Mixer",
category = Category.HomeGoods,
price = BigDecimal(299.99),
description = "A versatile and powerful stand mixer for baking and cooking"
)
val customer1 = Customer(
id = 1L,
name = "John Doe",
email = "[email protected]",
address = Address(
street = "123 Main St",
city = "Anytown",
state = "CA",
zip = "12345"
)
)
val customer2 = Customer(
id = 2L,
name = "Jane Smith",
email = "[email protected]",
address = Address(
street = "456 Elm St",
city = "Othertown",
state = "NY",
zip = "67890"
)
)
val order1 = Order(
id = 1L,
customer = customer1,
items = List(
OrderItem(product1, 1),
OrderItem(product2, 2)
),
status = OrderStatus.Pending
)
val order2 = Order(
id = 2L,
customer = customer2,
items = List(
OrderItem(product3, 1)
),
status = OrderStatus.Shipped
)
val paymentMethod1 = PaymentMethod(
`type` = PaymentType.CreditCard,
details = Map(
"card_number" -> "1234-5678-9012-3456",
"expiration_date" -> "12/2026"
)
)
val paymentMethod2 = PaymentMethod(
`type` = PaymentType.PayPal,
details = Map(
"paypal_id" -> "[email protected]"
)
)
val payment1 = Payment(
method = paymentMethod1,
amount = BigDecimal(1259.97),
timestamp = java.time.Instant.parse("2025-01-03T12:30:45Z")
)
val payment2 = Payment(
method = paymentMethod2,
amount = BigDecimal(299.99),
timestamp = java.time.Instant.parse("2025-01-15T19:10:55Z")
)
val orderPayment1 = OrderPayment(
order = order1,
payment = payment1
)
val orderPayment2 = OrderPayment(
order = order2,
payment = payment2
)
val report = List(
orderPayment1,
orderPayment2
)
```

## Defining the codec

To derive a codec the report type (`List[OrderPayment]` type in our case) we will use `JsonCodecMaker.make` macros:
```scala
given JsonValueCodec[List[OrderPayment]] = JsonCodecMaker.make
```

An instance of this codec (also known as a type-class instance) is getting to be visible in the scope of subsequent
calls of parsing and serialization methods.

## Serialization

Now we are ready to serialize the report. Just need to define some entry point method and call `writeToString`.
We will also print resulting JSON to the system output to see it as a process output:

```scala
@main def gettingStarted: Unit =
val json = writeToString(report)
println(json)
```

From this moment you can run the script and see a long JSON string on the screen.

## Parsing

Having the JSON string in memory you can parse it using following lines that should be pasted to the end of the
`gettingStarted` method:
```scala
val parsedReport = readFromString(json)
println(parsedReport)
```

Now you can rerun the script and get additionally printed `toString` representation of a report parsed from JSON string.

If something gone wrong you can pick the final version of [a script for this tutorial](1-getting-started.scala) and then run it.

## Challenge
Experiment with different types of collections and try to find any collection type from the standard Scala library that
is not supported by `JsonCodecMaker.make` macros to derive codec for serialization *and* parsing.

## Recap
In this tutorial we learned basics for parsing and serialization of complex nested data structures using `scala-cli`.
Having definition of data structures and their instances in memory we need to:
1. Add dependency usage and package imports of `core` and `macros` modules of jsoniter-scala
2. Define `given` type-class instance of `JsonValueCodec` for top-level data structure
3. Call `writeToString` to serialize an instance of complex data structure to JSON representation
4. Call `readFromString` to parse a complex data structure from JSON representation
135 changes: 135 additions & 0 deletions docs/tutorials/1-getting-started.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
//> using scala 3
//> using dep "com.github.plokhotnyuk.jsoniter-scala::jsoniter-scala-core::2.31.3"
//> using compileOnly.dep "com.github.plokhotnyuk.jsoniter-scala::jsoniter-scala-macros::2.31.3"

import com.github.plokhotnyuk.jsoniter_scala.core._
import com.github.plokhotnyuk.jsoniter_scala.macros._

enum Category extends Enum[Category]:
case Electronics, Fashion, HomeGoods

case class Product(id: Long, name: String, category: Category, price: BigDecimal, description: String)

enum OrderStatus extends Enum[OrderStatus]:
case Pending, Shipped, Delivered, Cancelled

case class OrderItem(product: Product, quantity: Int)

case class Order(id: Long, customer: Customer, items: List[OrderItem], status: OrderStatus)

case class Customer(id: Long, name: String, email: String, address: Address)

case class Address(street: String, city: String, state: String, zip: String)

enum PaymentType extends Enum[PaymentType]:
case CreditCard, PayPal

case class PaymentMethod(`type`: PaymentType, details: Map[String, String]/*e.g. card number, expiration date*/)

case class Payment(method: PaymentMethod, amount: BigDecimal, timestamp: java.time.Instant)

case class OrderPayment(order: Order, payment: Payment)

val product1 = Product(
id = 1L,
name = "Apple iPhone 16",
category = Category.Electronics,
price = BigDecimal(999.99),
description = "A high-end smartphone with advanced camera and AI capabilities"
)
val product2 = Product(
id = 2L,
name = "Nike Air Max 270",
category = Category.Fashion,
price = BigDecimal(129.99),
description = "A stylish and comfortable sneaker with a full-length air unit"
)
val product3 = Product(
id = 3L,
name = "KitchenAid Stand Mixer",
category = Category.HomeGoods,
price = BigDecimal(299.99),
description = "A versatile and powerful stand mixer for baking and cooking"
)
val customer1 = Customer(
id = 1L,
name = "John Doe",
email = "[email protected]",
address = Address(
street = "123 Main St",
city = "Anytown",
state = "CA",
zip = "12345"
)
)
val customer2 = Customer(
id = 2L,
name = "Jane Smith",
email = "[email protected]",
address = Address(
street = "456 Elm St",
city = "Othertown",
state = "NY",
zip = "67890"
)
)
val order1 = Order(
id = 1L,
customer = customer1,
items = List(
OrderItem(product1, 1),
OrderItem(product2, 2)
),
status = OrderStatus.Pending
)
val order2 = Order(
id = 2L,
customer = customer2,
items = List(
OrderItem(product3, 1)
),
status = OrderStatus.Shipped
)
val paymentMethod1 = PaymentMethod(
`type` = PaymentType.CreditCard,
details = Map(
"card_number" -> "1234-5678-9012-3456",
"expiration_date" -> "12/2026"
)
)
val paymentMethod2 = PaymentMethod(
`type` = PaymentType.PayPal,
details = Map(
"paypal_id" -> "[email protected]"
)
)
val payment1 = Payment(
method = paymentMethod1,
amount = BigDecimal(1259.97),
timestamp = java.time.Instant.parse("2025-01-03T12:30:45Z")
)
val payment2 = Payment(
method = paymentMethod2,
amount = BigDecimal(299.99),
timestamp = java.time.Instant.parse("2025-01-15T19:10:55Z")
)
val orderPayment1 = OrderPayment(
order = order1,
payment = payment1
)
val orderPayment2 = OrderPayment(
order = order2,
payment = payment2
)
val report = List(
orderPayment1,
orderPayment2
)

given JsonValueCodec[List[OrderPayment]] = JsonCodecMaker.make

@main def gettingStarted: Unit =
val json = writeToString(report)
println(json)
val parsedReport = readFromString(json)
println(parsedReport)