blob: 18817d378236c5762783b9d9d4079479731d154b [file] [view]
---
title: Scala
sidebar_position: 7
id: scala
license: |
Licensed to the Apache Software Foundation (ASF) under one or more
contributor license agreements. See the NOTICE file distributed with
this work for additional information regarding copyright ownership.
The ASF licenses this file to You under the Apache License, Version 2.0
(the "License"); you may not use this file except in compliance with
the License. You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
---
Fory JSON supports Scala 2.13 and Scala 3 through the optional `fory-json-scala` artifact. The
module works on the ordinary JVM and GraalVM Native Image. Android is not supported.
## Setup
```sbt
libraryDependencies += "org.apache.fory" %% "fory-json-scala" % "1.6.1"
```
`ForyJsonScala.builder()` installs the Scala module and returns the standard Fory JSON builder:
```scala
import org.apache.fory.json.scala.ForyJsonScala
case class Person(name: String, age: Int = 18, aliases: List[String] = Nil)
val json = ForyJsonScala.builder().build()
val text = json.toJson(Person("Ada"))
val person = json.fromJson(text, classOf[Person])
```
Reuse the resulting `ForyJson` instance. It is immutable and thread-safe after construction.
## Case classes and annotations
Case classes are decoded by calling their full primary constructor. Fory invokes Scala's generated
constructor-default methods for missing defaulted parameters; it does not parse default expressions
or mutate constructor `val` fields. Defaults in later parameter lists receive the preceding
constructor arguments exactly as Scala defines them. A missing parameter without a default is an
error. Mutable body properties are applied after construction.
Fory JSON annotations can be placed directly on Scala constructor properties:
```scala
import org.apache.fory.json.annotation.{JsonCodec, JsonIgnore, JsonProperty}
case class Media(
@JsonProperty("media_uri") uri: String,
@JsonIgnore internalId: String = "hidden",
@JsonCodec(elementCodec = classOf[TagCodec]) tags: List[Tag] = Nil,
@JsonProperty(include = JsonProperty.Include.NON_NULL) title: String = null
)
```
`JsonIgnore` applies to fields, property methods, setter parameters, and selected constructor
parameters. `JsonCodec` child slots bind direct collection elements, `Option` content, and map keys
or values. All other Fory JSON annotations retain the behavior described in
[Annotations](annotations.md).
If a required non-defaulted reference parameter uses an inclusion rule that would omit `null`,
serialization rejects a null value. This guarantees that JSON written by Fory remains readable by
the same case-class schema.
## Supported Scala types
| Scala type | JSON representation |
| ----------------------------------------------------------- | ----------------------------------------------- |
| `Unit` | `null` |
| case class | object |
| singleton object | empty object |
| value class | underlying value |
| `Option[A]`, `Some[A]`, `None` | contained value or `null` |
| `Either[L, R]` | object containing exactly one `l` or `r` member |
| `List`, `Seq`, `Vector`, `Queue`, `ArraySeq`, buffers, sets | array |
| Scala maps, `IntMap`, `LongMap` | object |
| immutable and mutable `BitSet` | ascending integer array |
| `Tuple1` through `Tuple22` | fixed-length array |
| Scala 3 `EmptyTuple` | empty array |
| `BigInt`, `BigDecimal` | JSON number |
| Scala `StringBuilder` | string |
| `Range`, supported `NumericRange` | realized value array |
| `FiniteDuration`, `Duration` | fixed `length`/`unit` or `special` object |
| parameterless Scala 3 enum | string case name |
| Scala 2 `Enumeration` | string through an owner-bound codec |
Strict standard-library collections are reconstructed through their standard Scala builders.
`Either` writes compact `l` and `r` member names. Readers also accept the legacy `left` and
`right` member names.
Fory does not add a Scala-specific collection-size limit; the codecs use the same input-length,
depth, graph-memory, and read-progress limits as Fory JSON core. A sparse `BitSet` whose highest
index would require backing storage disproportionate to the available JSON input is rejected.
Lazy or process-local values are intentionally unsupported by the default module, including
`LazyList`, `Stream`, views, iterators, collection builders, `Try`, `Throwable`, `Future`, `Promise`,
`ExecutionContext`, `Deadline`, functions, reflection/compiler metadata, and regex values. Sorted
or custom collections need an exact application codec because their ordering or construction is
application configuration.
## Parameterized types
Use a complete `TypeRef` when reading a parameterized Scala type:
```scala
import org.apache.fory.reflect.TypeRef
val typeRef = new TypeRef[Map[String, Option[Int]]]() {}
val value = json.fromJson("""{"count":1}""", typeRef)
```
Scala raw strings can be passed directly to `fromJson`; JSON double quotes do not need backslash
escaping.
Scala value-type arguments can erase to `Object` in a normal JVM signature. `ScalaTypeRef` is a
compile-time type-token constructor that preserves those arguments on Scala 2.13 and Scala 3:
```scala
import org.apache.fory.json.scala.ScalaTypeRef
val rangeType = ScalaTypeRef[scala.collection.immutable.NumericRange[Int]]
val range = json.fromJson("[1,3,5,7]", rangeType)
```
`Some[Int]` is a valid declared type when supplied with its complete type argument. A non-null JSON
value decodes to `Some(value)`; JSON `null` is rejected for `Some[Int]` but decodes to `None` for
`Option[Int]`.
## Scala 2 Enumeration
Scala 2 erases the owning `Enumeration` from `Enumeration#Value`. Use `JsonEnumeration` to retain
the owner on a direct value, collection or array element, `Option` content, or map key/value:
```scala
import org.apache.fory.json.scala.JsonEnumeration
object Weekday extends Enumeration {
val Monday, Tuesday = Value
}
object Month extends Enumeration {
val January, February = Value
}
case class Schedule(
@JsonEnumeration(classOf[Weekday.type]) day: Weekday.Value,
@JsonEnumeration(element = classOf[Weekday.type]) days: List[Weekday.Value],
@JsonEnumeration(content = classOf[Month.type]) month: Option[Month.Value],
@JsonEnumeration(
mapKey = classOf[Weekday.type],
mapValue = classOf[Month.type]
) labels: Map[Weekday.Value, Month.Value]
)
```
Each slot describes one direct `Enumeration.Value` occurrence. `value` cannot be combined with a
child slot, and `element`, `content`, and map slots must match the annotated property's immediate
type shape. Invalid or conflicting declarations fail when the case-class metadata is created.
For a custom wire representation, extend `ScalaEnumerationCodec` and select the codec through
`@JsonCodec`. The codec also implements the map-key contract, so its class can be used in
`keyCodec`.
## Scala 3 closed enums and sealed hierarchies
A parameterless Scala 3 enum uses its case name as a JSON string. Add `derives ScalaJsonCodec` to an
enum with parameterized cases to define one closed wrapper-object representation for every case:
```scala
import org.apache.fory.json.scala.*
enum Result derives ScalaJsonCodec {
case Ok(value: String)
case Error(code: Int)
case Pending
}
val json = ForyJsonScala.builder().build()
```
The values above use `{"Ok":{"value":"ready"}}`, `{"Error":{"code":7}}`, and
`{"Pending":{}}`. The reader never accepts a class name or chooses a subtype from runtime
reflection. For a third-party enum that cannot add `derives`, derive and register its schema at the
builder call site:
```scala
val json = ForyJsonScala.builder().register[thirdparty.Result].build()
```
For a Scala 3 sealed trait or class, add an empty `JsonSubTypes` annotation and derive
`ScalaJsonCodec`:
```scala
import org.apache.fory.json.annotation.JsonSubTypes
import org.apache.fory.json.scala.*
@JsonSubTypes(property = "kind")
sealed trait Event derives ScalaJsonCodec
final case class Message(value: String) extends Event
case object Idle extends Event
```
This example uses `Message` and `Idle` as logical subtype names. Derivation recursively traverses
sealed branches. A concrete open class is one exact member and its descendants are not admitted; an
open abstract branch is rejected. A non-empty annotation value remains an explicit subset. Scala 2
sealed traits and classes are not supported by this inference feature.
### Packaging Derived Codecs in a Module
A library that supports several third-party Scala 3 enums can package their derived codecs in a
reusable module:
```scala
import org.apache.fory.json.{ForyJsonModule, ModuleContext}
import org.apache.fory.json.scala.*
object ThirdPartyJsonModule extends ForyJsonModule:
override def install(context: ModuleContext): Unit =
context.registerCodec(
classOf[thirdparty.Result],
ScalaJsonCodec.derived[thirdparty.Result]
)
val json =
ForyJsonScala.builder()
.withModule(ThirdPartyJsonModule)
.build()
```
The derivation is compiled as part of the module, so consumers only install the compiled module.
This is the reusable equivalent of calling `register[thirdparty.Result]` on one builder.
Modules are installed explicitly with `withModule`. Fory JSON does not scan the classpath or invoke
modules through `ServiceLoader`; explicit installation keeps the enabled codecs deterministic and
prevents an unrelated dependency from changing deserialization behavior. See
[Modules](modules.md) for the general module API and registration rules.
## GraalVM Native Image
The Scala module uses the same registration on the JVM and in a native image. Application models,
custom codecs, and derived enum or sealed schemas must be reachable when the native image is built.
Generate Fory codecs as part of the native-image build rather than adding general reflection
configuration.