scala.md
August 16, 2026 ยท View on GitHub
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
libraryDependencies += "org.apache.fory" %% "fory-json-scala" % "1.6.1"
ForyJsonScala.builder() installs the Scala module and returns the standard Fory JSON builder:
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:
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.
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:
import org.apache.fory.reflect.TypeRef
val typeRef = new TypeRef[Map[String, Option[Int]]]() {}
val value = json.fromJson("{\"count\":1}", typeRef)
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:
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:
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
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:
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:
val json = ForyJsonScala.builder().register[thirdparty.Result].build()
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:
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 for the general module API and registration rules.
GraalVM Native Image
The Scala module uses the same build-time module registration on the JVM and in a native image. Application models, custom codecs, and derived enum 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. No Scala compiler, TASTy reader, or runtime macro execution is required.