| 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.
libraryDependencies += "org.apache.fory" %% "fory-json-scala" % "1.7.6"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.
Use ForyJsonScala.builder().escapeNonAscii(true) to escape non-ASCII string contents and names
in compact and pretty output. The setting is fixed per instance; raw JSON remains verbatim.
See Non-ASCII escaping.
Use ForyJsonScala.builder().writeLongAsString(true) to emit Scala Long values, including
declared collection and map values, Option[Long], Long-backed value classes, and Java Long-like
wrappers as quoted decimal strings. Readers accept both quoted and unquoted integer tokens.
Use ScalaTypeRef when a parameterized declaration contains Long because normal JVM signatures
can erase Scala value-type arguments to Object.
Scala Array[Byte] uses Base64 strings by default. Use the standard builder's
byteArrayFormat to select numeric arrays or hexadecimal strings, including arrays inside
Option, Scala collections, and maps:
import org.apache.fory.json.annotation.JsonByteArray
import org.apache.fory.json.scala.{ForyJsonScala, ScalaTypeRef}
val hexJson = ForyJsonScala.builder().byteArrayFormat(JsonByteArray.Format.BASE16).build()
val bytesType = ScalaTypeRef[Array[Byte]]
val text = hexJson.toJson(Array[Byte](1, -2, 3), bytesType) // "\"01fe03\""
val bytes = hexJson.fromJson(text, bytesType)JsonByteArray on a field or getter, including a Mixin, overrides the default for that property.
See byte-array formats for the read/write contract.
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. When a constructor parameter has no explicit
default, an omitted property uses its type's default:
- Numeric values use zero, and Boolean values use
false. - Collections, maps, and arrays use empty values. Mutable defaults are fresh for each object.
Option[A]usesNone.- Other reference values, including strings and nested objects, use
null.
Mutable body properties retain their initializers when omitted and are applied after construction when present.
case class Options(value: Option[Int], selected: Option[Int] = Some(7))
case class Profile(age: Int, enabled: Boolean, tags: List[String], name: String)
json.fromJson("{}", classOf[Options]) // Options(None, Some(7))
json.fromJson("""{"selected":null}""", classOf[Options]) // Options(None, None)
json.fromJson("{}", classOf[Profile]) // Profile(0, false, List(), null)Use ForyJsonScala.builder().failOnMissingRequiredProperties(true).build() to reject missing ordinary
constructor properties without a declared default. For example, case class Request(id: Int) then
rejects {}, while case class Request(id: Int = 7) still reads it as Request(7). Option,
collection, map, and array properties retain their existing missing-value defaults. The option is
disabled by default and does not change writing or explicit null handling.
Explicit constructor defaults take precedence for omitted properties. With the default onNullRead(SET), JSON null
decodes as None for Option[A], even when its constructor default is Some(...).
Use ForyJsonScala.builder().onNullRead(JsonProperty.NullHandling.SKIP) to skip null property
occurrences and retain constructor defaults or body-property initializers. onContentNullRead
applies to immediate elements and map values of supported strict Scala collections, including
primitive collections. Per-property JsonProperty settings override the matching global default
and do not propagate into nested containers. See Read-side null handling.
A case class may be declared at the top level, or inside an object at any nesting depth, as long
as every enclosing scope is itself an object. A case class enclosed by a class, a trait, or a
method is rejected for both reading and writing, because Fory cannot reach the enclosing instance
or the companion it needs to rebuild the value. Construction requires a public JVM constructor
with a matching public companion apply; unsupported private constructor shapes are rejected.
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.
Property inclusion controls which values are written. Omitted properties use constructor or type
defaults when read, so omitting an empty string can restore null. Use ALWAYS when those values
must remain distinct. Under onNullRead(SET), JSON null keeps the declared type's normal null behavior; it
does not request a constructor default.
NON_EMPTY recognizes None and supported empty strict Scala sequences, sets, and maps, including
mutable collections and ranges. Filtering is shallow: Some(""), Some(Nil), Some(null), and
nonempty containers remain included. It does not traverse lazy collections to determine emptiness.
Root values, array elements, map entries, and tuple positions are not removed.
These Scala codecs implement isEmpty(writer, value). If you replace one with a custom codec,
override that method to preserve the desired omission behavior; its default returns false.
For example, a custom Option codec without that override retains None fields under NON_EMPTY.
Dynamic Any properties use the codec selected for the actual value, while a field-specific custom
codec controls its own empty check. See Custom empty values.
Explicitly authorize stable declared defaults with NON_DEFAULT:
import org.apache.fory.json.annotation.JsonProperty.Include
import org.apache.fory.json.annotation.{JsonInclude, JsonProperty}
import org.apache.fory.json.scala.ForyJsonScala
@JsonInclude(Include.NON_DEFAULT)
case class Request(
id: Int,
retries: Int = 3,
tags: List[String] = Nil
)
val json = ForyJsonScala.builder().build()
json.toJson(Request(0)) // {"id":0}
json.fromJson("""{"id":0}""", classOf[Request]) // Request(0, 3, Nil)
json.toJson(Request(0, retries = 0)) // {"id":0,"retries":0}id has no declared default, so class-level authorization keeps it in the output, including when
its value is zero. A reader's implicit zero, empty collection, or None fallback is not a declared
default.
Alternatively, omit the class annotation and place @JsonProperty(include = Include.NON_DEFAULT)
only on selected defaulted properties. Mixins support both forms. Global NON_DEFAULT is rejected.
Default expressions run during writing and must be deterministic and free of externally visible
side effects. For case class Limits(low: Int)(val high: Int = low + 1), the comparison for high
uses the object's actual low. For low=5, high=2, high is retained because its default is 6.
Unavailable dependencies of a selected default method cause a model-initialization error.
Properties without a supported compiler default method are retained under both class-level and
field-level NON_DEFAULT. This also applies to Unit defaults whose JVM methods return void,
which are not supported comparison sources.
Authorization confirms that missing input restores the same context; Fory does not prove this or
expression purity. Use ALWAYS for time-, random-, or state-dependent defaults. Arrays compare by contents, and
floating-point comparisons distinguish positive and negative zero. Class-body initializers are
not inferred as Scala constructor defaults and remain written. Values differing from a default,
including null and empty collections, remain written. Reading stays independent and creates fresh mutable defaults.
Class authorization covers future added fields too; see Default omission.
| 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 |
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.
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 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:
import org.apache.fory.json.scala.ScalaTypeRef
val rangeType = ScalaTypeRef[scala.collection.immutable.NumericRange[Int]]
val range = json.fromJson("[1,3,5,7]", rangeType)Generic case classes preserve their type arguments, including finite nesting of the same class:
case class Box[A](value: A)
val boxType = ScalaTypeRef[Box[Box[Int]]]
val box = json.fromJson("""{"value":{"value":1}}""", boxType)
json.toJson(box, boxType) // {"value":{"value":1}}Recursive declarations that continually expand their type arguments, such as Node[A] containing
Node[List[A]], need a custom codec.
Use ScalaTypeRef[Unit] for a Unit root value:
val unitType = ScalaTypeRef[Unit]
json.toJson((), unitType) // "null"
json.fromJson("null", unitType) // ()Unit also works in case-class fields and nested types such as List[Unit], Array[Unit],
and Option[Unit]. Each Unit value is encoded as JSON null. Under the value-or-null
representation of Option, Some(()) writes null and reads back as None.
Do not pass classOf[Unit] to the Java Class overload: it denotes JVM void, which is
rejected when writing a root value.
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].
Use ScalaTypeRef on Scala 2.13 or Scala 3 to preserve a statically known enumeration owner,
including inside arrays, collections, options, and maps:
import org.apache.fory.json.scala.{ForyJsonScala, ScalaTypeRef}
object Suit extends Enumeration {
val Hearts, Clubs = Value
}
val json = ForyJsonScala.builder().build()
val suits = ScalaTypeRef[Array[Suit.Value]]
val values = json.fromJson("""["Hearts","Clubs"]""", suits)
val text = json.toJson(values, suits)The owner is selected for each type occurrence. Different enumerations can coexist in one runtime;
no registration for the shared Enumeration.Value class is needed. Type aliases that retain the
owner also work. An erased Enumeration#Value, Class, or ordinary JVM TypeRef cannot recover
the owner. In particular, case-class properties discovered through JVM reflection need
JsonEnumeration when their signature has erased the owner. Use this annotation 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.
On Scala 2.13 and Scala 3, explicitly select ScalaJsonCodec.stringEnum[T] to encode a closed
sealed hierarchy of singleton cases as JSON strings:
import org.apache.fory.json.scala.{ForyJsonScala, ScalaJsonCodec, ScalaTypeRef}
sealed trait Color
case object Red extends Color
case object Blue extends Color
val json = ForyJsonScala.builder()
.registerCodec(classOf[Color], ScalaJsonCodec.stringEnum[Color])
.build()
val colors = ScalaTypeRef[Array[Color]]
val text = json.toJson(Array[Color](Red, Blue), colors) // ["Red","Blue"]
val values = json.fromJson(text, colors)The compiler discovers the cases and their names, including cases beneath sealed intermediate
branches. No handwritten name-to-member mapping is required. An open abstract branch or a case
with constructor parameters is rejected at compilation. Unknown input names are rejected; names
never identify classes to load. Case names do not depend on an overridden toString.
This representation is opt-in. ScalaJsonCodec.derived[T] retains the wrapper-object representation
and is also available for explicitly registered Scala 2 sealed hierarchies. Scala 3 derives and
parameterless Scala 3 enum defaults are unchanged. null remains JSON null.
Use JsonFormat(shape = JsonFormat.Shape.STRING) on a Boolean or numeric property to write its
scalar token as a JSON string. Reading accepts both strings and native scalar tokens:
import org.apache.fory.json.annotation.{JsonFormat, JsonMixin}
import org.apache.fory.json.scala.ForyJsonScala
case class Artifact(expired: Boolean, size: Long)
@JsonMixin(target = classOf[Artifact])
abstract class ArtifactMixin {
@JsonFormat(shape = JsonFormat.Shape.STRING) var expired: Boolean = false
}
val json = ForyJsonScala.builder().registerMixin(classOf[ArtifactMixin]).build()
val text = json.toJson(Artifact(false, 7L)) // {"expired":"false","size":7}The annotation may instead be placed directly on a constructor property. A Mixin keeps the model
unchanged and uses only Fory annotations. Scala BigInt and BigDecimal are supported along with
primitive and boxed Boolean/numeric types. See Annotations for null,
non-finite number, and supported direct-wrapper behavior.
A parameterless Scala 3 enum uses its case name as a JSON string, including as the key of a typed
Scala map such as Map[Color, 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()For a Scala 3 sealed trait or class, add an empty JsonSubTypes annotation and derive
ScalaJsonCodec:
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 EventThis 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.
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.
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.