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 typeJSON representation
Unitnull
case classobject
singleton objectempty object
value classunderlying value
Option[A], Some[A], Nonecontained value or null
Either[L, R]object containing exactly one l or r member
List, Seq, Vector, Queue, ArraySeq, buffers, setsarray
Scala maps, IntMap, LongMapobject
immutable and mutable BitSetascending integer array
Tuple1 through Tuple22fixed-length array
Scala 3 EmptyTupleempty array
BigInt, BigDecimalJSON number
Scala StringBuilderstring
Range, supported NumericRangerealized value array
FiniteDuration, Durationfixed length/unit or special object
parameterless Scala 3 enumstring case name
Scala 2 Enumerationstring 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.