Back to Smile

SMILE JSON

json/README.md

6.1.028.1 KB
Original Source

SMILE JSON

smile-json is a lightweight, clean, and efficient JSON library for Scala. It is built entirely within the SMILE project and has no external runtime dependencies.

Key capabilities:

  • A mutable model of JSON objects and arrays, making it practical for use in database-oriented code where documents are updated in place.
  • An efficient recursive-descent parser (adapted from spray-json) that reads from String, Array[Char], or Array[Byte] (UTF-8) inputs.
  • Compact and pretty-print serialization.
  • String interpolators (json""" """ and jsan""" """) with embedded variable support.
  • Beyond standard JSON: native types for Int, Long, Counter, BigDecimal, Instant/LocalDate/LocalTime/LocalDateTime, Timestamp, UUID, ObjectId (BSON), and Binary.
  • BSON-compatible binary serialization via JsonSerializer, suitable for efficient on-wire or on-disk storage.
  • Implicit conversions in both directions so that Scala values slot into JSON structures without boilerplate.

Table of Contents

  1. Installation
  2. The Type Hierarchy
  3. Parsing JSON
  4. Navigating a Document
  5. Mutating a Document
  6. Building Documents Programmatically
  7. Serializing to Text
  8. Implicit Conversions
  9. Extended Type System
  10. JsArray Operations
  11. Binary Serialization (BSON)
  12. ObjectId
  13. Complete Examples

Installation

Add the dependency in build.gradle.kts:

kotlin
dependencies {
    implementation(project(":json"))
}

Then in every Scala file that uses the library:

scala
import smile.json.*

This single import brings in the string interpolators, all JsValue types, and all implicit conversions.


The Type Hierarchy

All JSON values extend the sealed trait JsValue.

JsValue
├── JsNull          — JSON null
├── JsUndefined     — sentinel for missing/absent values
├── JsBoolean       — true / false  (aliases: JsTrue, JsFalse)
├── JsInt           — 32-bit integer
├── JsLong          — 64-bit integer
├── JsCounter       — 64-bit integer (56 effective bits; database counter)
├── JsDouble        — IEEE 754 double
├── JsDecimal       — arbitrary-precision decimal (java.math.BigDecimal)
├── JsString        — Unicode text
├── JsDate          — instant (java.time.Instant, milliseconds since epoch)
├── JsLocalDate     — date without time (java.time.LocalDate)
├── JsLocalTime     — time without date (java.time.LocalTime)
├── JsLocalDateTime — date + time without zone (java.time.LocalDateTime)
├── JsTimestamp     — SQL TIMESTAMP with nanosecond precision
├── JsUUID          — java.util.UUID
├── JsObjectId      — BSON-style 12-byte ObjectId
├── JsBinary        — raw byte array
├── JsObject        — mutable ordered map of String → JsValue
└── JsArray         — mutable indexed sequence of JsValue

JsValue extends scala.Dynamic, so dot-notation field access is supported on JsObject without any code generation.


Parsing JSON

String interpolators (literal strings)

The json""" """ interpolator parses a string literal into a JsObject. The jsan""" """ interpolator parses a string literal into a JsArray.

scala
import smile.json.*

val doc = json"""
  {
    "store": {
      "book": [
        {
          "category": "reference",
          "author":   "Nigel Rees",
          "title":    "Sayings of the Century",
          "price":    8.95
        },
        {
          "category": "fiction",
          "author":   "Evelyn Waugh",
          "title":    "Sword of Honour",
          "price":    12.99
        },
        {
          "category": "fiction",
          "author":   "Herman Melville",
          "title":    "Moby Dick",
          "isbn":     "0-553-21311-3",
          "price":    8.99
        },
        {
          "category": "fiction",
          "author":   "J. R. R. Tolkien",
          "title":    "The Lord of the Rings",
          "isbn":     "0-395-19395-8",
          "price":    22.99
        }
      ],
      "bicycle": {
        "color": "red",
        "price": 19.95
      }
    }
  }
  """

val arr = jsan"""[1, 2, 3, "hello"]"""

Both interpolators support embedded variable references:

scala
val title  = "Effective Scala"
val price  = 29.99
val author = "Li Haoyi"

val book = json"""
  {
    "title":  $title,
    "price":  $price,
    "author": $author
  }
  """

Parsing a string variable

When the JSON text is in a runtime variable rather than a literal, use the parseJson or parseJsObject extension methods on String (brought in by import smile.json.*):

scala
val raw = """{"x": 1, "y": 2}"""

val value:  JsValue  = raw.parseJson       // any JsValue
val obj:    JsObject = raw.parseJsObject   // JsObject (throws if not an object)

parseJson handles any valid JSON expression:

scala
"42".parseJson          // JsInt(42)
"3.14".parseJson        // JsDouble(3.14)
"true".parseJson        // JsBoolean(true)
"null".parseJson        // JsNull
"[1,2,3]".parseJson     // JsArray

Parser input variants

JsonParser also accepts Array[Char] and Array[Byte] (UTF-8) directly:

scala
JsonParser(charArray)   // Array[Char]
JsonParser(byteArray)   // Array[Byte] (UTF-8)
JsonParser(string)      // String

Number literal suffixes

The parser recognises two non-standard numeric suffixes for unambiguous type selection in JSON text:

SuffixType producedExample
l or LJsLong42L
c or CJsCounter0C

Without a suffix, integers that fit in 32 bits become JsInt; larger integers automatically become JsLong.

Automatic recognition of UUIDs and ObjectIds

During parsing, any JSON string whose value exactly matches the UUID pattern (xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx) is transparently lifted to JsUUID, and any string matching ObjectId(…) (24 hex digits) is lifted to JsObjectId.


Bracket notation

scala
doc("store")("bicycle")("color")       // JsString("red")
doc("store")("book")(0)("author")      // JsString("Nigel Rees")
doc("store")("book")(2)("isbn")        // JsString("0-553-21311-3")

Negative indices count from the end:

scala
doc("store")("book")(-1)("title")      // last book: "The Lord of the Rings"

Range slices on arrays:

scala
val books = doc("store")("book").asInstanceOf[JsArray]
books(0, 2)        // first two books (exclusive end)
books(0, 4, 2)     // every other book: indices 0 and 2
books(1 until 3)   // books at indices 1 and 2

Dot notation (Dynamic)

Because JsValue extends scala.Dynamic, field access reads naturally as if the object had named methods:

scala
doc.store.bicycle.color            // JsString("red")
doc.store.book(0).author           // JsString("Nigel Rees")
doc.store.book(1).price.asDouble   // 12.99

Note: because Scala is statically typed, doc.store.book has the static type JsValue. Use asInstanceOf[JsArray] (or a pattern match) before calling array-specific methods like foreach.

Missing fields — JsUndefined

Accessing a field that does not exist returns JsUndefined rather than throwing:

scala
doc.book              // JsUndefined  (correct path is doc.store.book)
doc.store.bike.color  // JsUndefined  (correct key is "bicycle")

Use JsObject.contains to test presence before reading:

scala
val obj = doc("store").asInstanceOf[JsObject]
if (obj.contains("bicycle")) …

Or use the safe get method that returns Option[JsValue]:

scala
doc("store").asInstanceOf[JsObject].get("bicycle")   // Some(JsObject(...))
doc("store").asInstanceOf[JsObject].get("missing")   // None

Extracting Scala values

Each JsValue subtype exposes asXxx methods to extract the underlying value:

scala
val price: Double = doc.store.bicycle.price.asDouble
val color: String = doc.store.bicycle.color.asInt      // UnsupportedOperationException: not an Int

Available extractors:

MethodReturn type
asBooleanBoolean
asIntInt
asLongLong
asDoubleDouble
asDecimaljava.math.BigDecimal
asInstantjava.time.Instant
asLocalDatejava.time.LocalDate
asLocalTimejava.time.LocalTime
asLocalDateTimejava.time.LocalDateTime
asTimestampjava.sql.Timestamp

For the numeric types (JsInt, JsLong, JsDouble, JsCounter, JsDecimal) cross-type coercions work:

scala
JsInt(42).asDouble       // 42.0
JsDouble(3.0).asInt      // 3
JsLong(1000L).asInt      // 1000

Mutating a Document

JsObject and JsArray are mutable. This is intentional — it makes the library practical for in-place document updates that are common in database workflows.

Setting / adding a field

Assign through dot notation:

scala
doc.store.bicycle.color = "green"

Or bracket notation:

scala
doc("store")("bicycle")("color") = JsString("green")

The right-hand side is implicitly converted from common Scala types:

scala
doc.store.bicycle.price  = 24.99          // Double → JsDouble
doc.store.bicycle.inStock = true          // Boolean → JsBoolean
doc.store.bicycle.year   = 2024           // Int → JsInt
doc.store.bicycle.serial = "SN-12345"     // String → JsString

Removing a field

scala
// returns Option[JsValue] — the removed value, or None if absent
val removed: Option[JsValue] = doc.store.book(0).asInstanceOf[JsObject].remove("price")

// Setting to JsUndefined has the same effect but returns nothing
doc.store.book(0).price = JsUndefined

Deep merge

JsObject supports deep merging with ++=:

scala
val patch = json"""{"bicycle": {"color": "blue", "gears": 21}}"""
doc.store.asInstanceOf[JsObject] ++= patch

// "color" is overwritten; "gears" is added; other fields are untouched

Nested JsObject values are merged recursively; all other types are replaced.


Building Documents Programmatically

JsObject

scala
// Vararg key–value pairs (insertion order preserved)
val obj = JsObject(
  "name"  -> JsString("Alice"),
  "age"   -> JsInt(30),
  "score" -> JsDouble(9.5)
)

// From an immutable Map
val map: Map[String, JsValue] = Map("x" -> JsInt(1), "y" -> JsInt(2))
val obj = JsObject(map)

// From a mutable map — implicit conversion
import scala.collection.mutable
val mmap = mutable.Map("key" -> (JsString("val"): JsValue))
val obj: JsObject = mmap   // implicit conversion

Scala maps of primitive types are converted implicitly via toJsObject:

scala
val m: Map[String, Int] = Map("a" -> 1, "b" -> 2)
val obj: JsObject = m.toJsObject   // Map[String, Int] → JsObject

// Available for: Boolean, Int, Long, Double, BigDecimal, String, Date

JsArray

scala
// Vararg elements
val arr = JsArray(JsInt(1), JsDouble(2.5), JsString("three"))

// Implicit conversion from Array[T <: JsValue] or Seq[T <: JsValue]
val arr: JsArray = Array(JsInt(1), JsInt(2), JsInt(3))
val arr: JsArray = List(JsString("a"), JsString("b"))

Sequences of primitive types are converted via toJsArray:

scala
val ints: Seq[Int] = Seq(1, 2, 3)
val arr: JsArray = ints.toJsArray   // Seq[Int] → JsArray

// Available for: Boolean, Int, Long, Double, BigDecimal, String,
//               LocalDate, LocalTime, LocalDateTime, Date, Timestamp

Serializing to Text

Compact (default)

toString and compactPrint both produce compact output with no extra whitespace:

scala
val doc = json"""{"a": 1, "b": [2, 3]}"""

doc.toString       // {"a":1,"b":[2,3]}
doc.compactPrint   // identical

Pretty-print

scala
doc.prettyPrint

produces indented output:

json
{
  "a": 1,
  "b": [
    2,
    3
  ]
}

JSON Lines (newline-delimited JSON)

JsArray has a convenience method for JSONL output, one element per line:

scala
val arr = jsan"""[{"id":1},{"id":2},{"id":3}]"""
println(arr.jsonl)
// {"id":1}
// {"id":2}
// {"id":3}

Implicit Conversions

import smile.json.* activates two sets of implicit conversions so you almost never need to construct JsXxx wrapper types explicitly.

Scala → JsValue (lift)

Scala typeJsValue type
BooleanJsBoolean
IntJsInt
LongJsLong
DoubleJsDouble
java.math.BigDecimalJsDecimal
StringJsString
java.time.InstantJsDate
java.time.LocalDateJsLocalDate
java.time.LocalTimeJsLocalTime
java.time.LocalDateTimeJsLocalDateTime
java.sql.TimestampJsTimestamp
java.util.DateJsTimestamp
java.util.UUIDJsUUID
ObjectIdJsObjectId
Array[Byte]JsBinary
Array[T <: JsValue]JsArray
Seq[T <: JsValue]JsArray
Seq[(String, T <: JsValue)]JsObject
Map[String, T <: JsValue]JsObject

JsValue → Scala (lower)

JsValue typeScala type
JsBooleanBoolean
JsIntInt
JsLongLong
JsDoubleDouble
JsDecimaljava.math.BigDecimal
JsStringString
JsDatejava.time.Instant
JsLocalDatejava.time.LocalDate
JsLocalTimejava.time.LocalTime
JsLocalDateTimejava.time.LocalDateTime
JsTimestampjava.sql.Timestamp (also java.util.Date)
JsObjectIdObjectId
JsUUIDjava.util.UUID
JsBinaryArray[Byte]

These lowering conversions allow JsXxx values to be passed directly to any Java/Scala API that expects the underlying type:

scala
val price: Double = doc.store.bicycle.price   // implicit JsDouble → Double
val color: String = doc.store.bicycle.color   // implicit JsString → String

Collection convenience conversions

Sequences and maps of primitive types gain toJsArray / toJsObject extension methods:

scala
Seq(true, false, true).toJsArray
Array(1, 2, 3).toJsArray
Seq(1.0, 2.0, 3.0).toJsArray
Seq("a", "b").toJsArray
Map("x" -> 1, "y" -> 2).toJsObject
Map("ts" -> java.time.LocalDate.now()).toJsObject

Extended Type System

Beyond the six standard JSON types (null, boolean, number, string, array, object) smile-json supports these extended value types:

Numeric types

TypeWhen used
JsInt32-bit integer (parsed automatically when value fits)
JsLong64-bit integer (auto-promoted when value overflows Int; literal suffix L)
JsCounter64-bit integer, semantically a counter; literal suffix C
JsDoubleIEEE 754 double (any number containing ., e, or E)
JsDecimalArbitrary-precision decimal
scala
JsInt(42)
JsLong(Long.MaxValue)
JsCounter(0L)
JsDouble(3.14)
JsDecimal("1234567890.123456789012345678901234567890")
JsDecimal(new java.math.BigDecimal("9.99"))

Temporal types

All temporal types round-trip through text as ISO-8601 strings; in BSON they are stored as compact integers.

scala
import java.time.*

JsDate(Instant.now())                          // UTC instant (milliseconds)
JsDate(epochMillis: Long)
JsDate("2024-01-15T10:30:00Z")                 // ISO-8601 string

JsLocalDate(LocalDate.of(2024, 1, 15))
JsLocalDate(epochDay: Long)
JsLocalDate("2024-01-15")

JsLocalTime(LocalTime.of(10, 30, 0))
JsLocalTime(nanoOfDay: Long)
JsLocalTime("10:30:00")

JsLocalDateTime(LocalDateTime.of(2024, 1, 15, 10, 30))
JsLocalDateTime(date: LocalDate, time: LocalTime)
JsLocalDateTime("2024-01-15T10:30:00")

JsTimestamp(java.sql.Timestamp.from(Instant.now()))   // nanosecond precision
JsTimestamp(epochMillis: Long)
JsTimestamp("2024-01-15 10:30:00.123456789")          // JDBC escape format

Precision note: JsLocalTime and JsLocalDateTime truncate to-second precision in BSON storage (nanoseconds are dropped). Use JsTimestamp when sub-second precision matters.

Identity types

scala
// UUID — auto-detected when parsing strings of length 36
JsUUID()                                     // random UUID
JsUUID(UUID.randomUUID())
JsUUID(mostSigBits: Long, leastSigBits: Long)
JsUUID("550e8400-e29b-41d4-a716-446655440000")
JsUUID(bytes: Array[Byte])                   // name-based UUID from bytes

// ObjectId (BSON) — auto-detected when parsing "ObjectId(…)" strings
JsObjectId()                                 // generate new ObjectId
JsObjectId(ObjectId.generate)
JsObjectId("507f1f77bcf86cd799439011")       // 24-char hex string
JsObjectId(bytes: Array[Byte])

// Binary
JsBinary(Array[Byte](0x48, 0x65, 0x6c, 0x6c, 0x6f))

Comparison and ordering

All concrete JsValue subtypes that represent scalar values implement Ordered[T], so they can be sorted and compared with <, >, <=, >=. The JsValueOrdering can sort a heterogeneous collection by attempting numeric comparison first, falling back to string comparison:

scala
import scala.util.Sorting
val values: Array[JsValue] = Array(JsInt(3), JsDouble(1.5), JsInt(2))
Sorting.quickSort(values)(JsValueOrdering)
// → JsDouble(1.5), JsInt(2), JsInt(3)

JsArray Operations

JsArray implements Iterable[JsValue], giving access to all standard Scala collection operations.

Appending elements

scala
val a = JsArray(JsInt(1), JsInt(2), JsInt(3))

a += JsInt(4)                        // append one element
a += JsInt(5)
a ++= JsArray(JsInt(6), JsInt(7))    // append another array
a ++= List(JsInt(8), JsInt(9))       // append any IterableOnce

Prepending elements

scala
JsInt(0) +=: a              // prepend one element
JsArray(JsInt(-1)) ++=: a   // prepend another array

Inserting at an index

scala
a.insertAll(2, Iterable(JsString("inserted"), JsString("here")))

Removing elements

scala
a.remove(0)          // remove and return element at index 0
a.remove(-1)         // remove last element
a.remove(1, 2)       // remove 2 elements starting at index 1

// Setting to JsUndefined does NOT shrink the array
a(0) = JsUndefined   // keeps size; element becomes undefined

Updating in place

scala
a(2) = JsString("replaced")

Iteration and functional operations

scala
val books = doc.store.book.asInstanceOf[JsArray]

books.foreach { b => println(b.title) }

val titles: Iterable[JsValue] = books.map(_.title)
val cheap  = books.filter(_.price.asDouble < 10.0)
val total  = books.map(_.price.asDouble).sum
val found  = books.find(_.author == "Herman Melville")

books.forall(_.price.asDouble > 0.0)
books.exists(_.category == "fiction")
books.size
books.isEmpty

Binary Serialization (BSON)

JsonSerializer encodes any JsValue to a compact binary format based on the BSON specification, with extensions for the extra types not covered by BSON.

Key characteristics:

  • Root can be any JsValue (BSON requires a document root; smile-json relaxes this).
  • JsLocalTime and JsLocalDateTime lose sub-second precision (stored as packed integers for space efficiency).
  • JsCounter is not supported in the binary format (throws IllegalArgumentException).
  • Default buffer size is 10 MB. Pass a pre-allocated ByteBuffer for different sizes.
  • Not thread-safe — each thread needs its own instance.
scala
val serializer = new JsonSerializer()           // 10 MB buffer (default)
val big        = new JsonSerializer(ByteBuffer.allocate(50 * 1024 * 1024))  // 50 MB

// Serialize any JsValue to Array[Byte]
val bytes: Array[Byte] = serializer.serialize(doc)

// Deserialize back
val restored: JsValue = serializer.deserialize(bytes)

// Or from a ByteBuffer directly
val restored: JsValue = serializer.deserialize(byteBuffer)

// Reuse the internal buffer (useful in tight loops)
serializer.clear()
val bytes = serializer.serialize(nextDoc)

BSON type codes

The serializer uses standard BSON type bytes where possible and reserves custom bytes for the extended types:

ByteType
0x01JsDouble
0x02JsString
0x03JsObject
0x04JsArray
0x05JsBinary / JsUUID
0x07JsObjectId
0x08JsBoolean
0x09JsDate (UTC millis)
0x0AJsNull
0x10JsInt
0x12JsLong
0x20JsLocalDate
0x21JsLocalTime
0x22JsLocalDateTime
0x23JsTimestamp
0x30JsDecimal
0x06JsUndefined

ObjectId

ObjectId is a 12-byte BSON identifier composed of:

┌───────────────┬────────────────┬────────────────┬──────────────┐
│  timestamp    │  machine id    │  thread id     │  increment   │
│   (4 bytes)   │   (3 bytes)    │   (2 bytes)    │  (3 bytes)   │
└───────────────┴────────────────┴────────────────┴──────────────┘

This structure means ObjectIds are naturally sortable by creation time.

scala
// Generate a new ObjectId
val id = ObjectId.generate         // or: ObjectId()

// Construct from a 24-character hex string
val id = ObjectId("507f1f77bcf86cd799439011")

// Safe parse (returns Try)
val idOpt: scala.util.Try[ObjectId] = ObjectId.parse("507f1f77bcf86cd799439011")

// Generate with a specific timestamp (for range queries)
val anchor = ObjectId.fromTime(System.currentTimeMillis, fillOnlyTimestamp = true)

// Extract creation time
val createdAt: java.util.Date = id.timestamp

// String representation
id.toString   // ObjectId(507F1F77BCF86CD799439011)

JsObjectId wraps ObjectId and is recognised automatically during JSON parsing. Any string of exactly the form ObjectId(<24 hex chars>) is parsed to JsObjectId — no special annotation needed.


Complete Examples

Example 1 — Parse, navigate, and pretty-print

scala
import smile.json.*

val doc = json"""
  {
    "store": {
      "book": [
        {"title": "Scala Programming",    "price": 34.99, "pages": 450},
        {"title": "Functional Design",    "price": 42.00, "pages": 380},
        {"title": "Type-Driven Dev",      "price": 39.50, "pages": 410}
      ],
      "bicycle": {"color": "red", "price": 19.95}
    }
  }
  """

// Navigate with dot notation
println(doc.store.bicycle.color)           // red
println(doc.store.book(0).title)           // Scala Programming

// Navigate with bracket notation
println(doc("store")("book")(1)("price"))  // 42.0

// Iterate the array
doc.store.book.asInstanceOf[JsArray].foreach { b =>
  println(s"${b.title} — $$${b.price.asDouble}")
}

// Pretty-print
println(doc.prettyPrint)

Example 2 — Build a document programmatically

scala
import smile.json.*
import java.util.UUID

// Construct via JsObject literal syntax
val user = JsObject(
  "_id"       -> JsObjectId(),
  "name"      -> JsString("Alice"),
  "age"       -> JsInt(30),
  "scores"    -> Seq(98, 87, 93).toJsArray,      // Seq[Int] → JsArray
  "metadata"  -> Map("role" -> "admin", "team" -> "ml").toJsObject,
  "sessionId" -> JsUUID(UUID.randomUUID())
)

println(user.prettyPrint)

// Or build via mutation
val record = JsObject()
record.name   = "Bob"              // String auto-lifted to JsString
record.active = true               // Boolean auto-lifted to JsBoolean
record.score  = 4.8                // Double auto-lifted to JsDouble

Example 3 — Mutate a nested document

scala
import smile.json.*

val inventory = json"""
  {
    "items": [
      {"sku": "A1", "qty": 10, "price": 5.00},
      {"sku": "B2", "qty": 3,  "price": 12.50}
    ]
  }
  """

val items = inventory.items.asInstanceOf[JsArray]

// Update a field in place
items(0).asInstanceOf[JsObject]("qty") = JsInt(15)

// Remove a field
items(1).asInstanceOf[JsObject].remove("price")

// Append a new item
items += json"""{"sku": "C3", "qty": 7, "price": 8.00}"""

println(inventory.prettyPrint)

Example 4 — Embedded variable interpolation

scala
import smile.json.*
import java.time.LocalDate

case class Product(sku: String, name: String, price: Double, launched: LocalDate)

def toJson(p: Product): JsObject = json"""
  {
    "sku":      ${p.sku},
    "name":     ${p.name},
    "price":    ${p.price},
    "launched": ${p.launched}
  }
  """

val product = Product("X9", "Widget Pro", 24.99, LocalDate.of(2024, 3, 1))
println(toJson(product).prettyPrint)
// "launched" is stored as JsLocalDate, serialized as "2024-03-01"

Example 5 — Binary serialization round-trip

scala
import smile.json.*

val doc = json"""{"user": "Alice", "score": 99, "active": true}"""

val serializer = new JsonSerializer()

// Encode to binary (BSON-compatible)
val bytes: Array[Byte] = serializer.serialize(doc)
println(s"Encoded: ${bytes.length} bytes")

// Decode back
val restored = serializer.deserialize(bytes).asInstanceOf[JsObject]
println(restored.prettyPrint)

assert(restored("user")   == JsString("Alice"))
assert(restored("score")  == JsInt(99))
assert(restored("active") == JsBoolean(true))

Example 6 — Sorting and filtering an array

scala
import smile.json.*

val catalog = jsan"""
  [
    {"title": "Moby Dick",    "price": 8.99},
    {"title": "Hamlet",       "price": 4.50},
    {"title": "War & Peace",  "price": 14.99}
  ]
  """

// Filter books under $10
val affordable = catalog.filter(_.price.asDouble < 10.0)
affordable.foreach(b => println(b.title))

// Sort by price
val sorted = catalog.toSeq.sortBy(_.price.asDouble)
sorted.foreach(b => println(s"${b.title}: $$${b.price.asDouble}"))

// Total price
val total = catalog.map(_.price.asDouble).sum
println(f"Total: $$${total}%.2f")

Example 7 — Working with ObjectId and temporal types

scala
import smile.json.*
import java.time.{Instant, LocalDate}

// Create a document with identity and temporal fields
val event = JsObject(
  "_id"       -> JsObjectId(),
  "type"      -> JsString("purchase"),
  "createdAt" -> JsDate(Instant.now()),
  "date"      -> JsLocalDate(LocalDate.now()),
  "amount"    -> JsDecimal("123.456789012345")   // arbitrary precision
)

// Extract the creation timestamp from the ObjectId
val oid = event("_id").asInstanceOf[JsObjectId].value
println(s"Created: ${oid.timestamp}")

// Round-trip through BSON
val serializer = new JsonSerializer()
val bytes = serializer.serialize(event)
val restored = serializer.deserialize(bytes)

println(restored.prettyPrint)

Design Notes

Mutability

Unlike most Scala JSON libraries (Circe, play-json, ujson in immutable mode), smile-json deliberately makes JsObject and JsArray mutable. This choice is driven by database use cases where partial document updates are common and copying the entire document on each change would be costly.

If you need value semantics, perform deep copies manually or work with prettyPrint/compactPrint and re-parse.

Field ordering

JsObject uses collection.mutable.SeqMap internally, so insertion order is preserved in both serialization and iteration — consistent with the modern JSON specification and important for reproducible output.

Number representation

The parser preserves the distinction between integers and doubles:

  • Integers that fit in 32 bits → JsInt
  • Integers that overflow 32 bits → JsLong
  • Numbers with ., e, or EJsDouble

This means round-tripping 42 through the parser always yields JsInt(42), not JsDouble(42.0), which avoids unintended precision loss and matches what most database drivers expect.

JsUndefined vs JsNull

ValueMeaning
JsNullExplicit JSON null — a value is present and is null
JsUndefinedField absent or sentinel for deletion

Setting a field to JsUndefined removes it from the next serialization of a JsObject. On JsArray, setting an element to JsUndefined does not shrink the array; use remove(index) for that.


SMILE — Copyright © 2010–2026 Haifeng Li. GNU GPL licensed.