简介

wjson 是一个 Scala 3 的 JSON 处理库,提供了基本的 JSON 构建、解析、操作的功能,并提供了对 ADT 数据类型的 JSON 映射支持,

  1. wjson-core 提供了基本的 JSON 操作 API
    • JsValue API: 对 JSON 数据结构的 ADT 抽象,支持 immutable 风格的 build, access, show 等操作,是 wjson 的核心数据结构。
    • JSON parer, 一个高性能的JSON解析器,支持标准的 JSON 语法
    • JSON5 parser, JSON5 语法解析器。JSON5 是 JSON 的超集,支持注释、多行字符串等特性。
    • json"..." 和 json5"..." 字符串插值
    • ADT 支持
      • product type (case class)
      • sum type (enum)
      • or type (union)
  2. wjson-pattern 一个实验性的 JSON 模式匹配库,非常适合于从 JSON 中快速提取信息。
    • jsp"..." 提供了一个 JSON Pattern DSL 语言,可以匹配复杂的 JSON 数据结构
    • 匹配 JSON 数据结构 并 提取数据
  3. wjson-schema 一个实验性的 JSON Schema support 库
    • 对 ADT 类型生成 JSON Schema
    • 提供一组 annotation 用于定义 JSON Schema 的约束

why another JSON library?

  1. 更好的 ADT 支持, 相比其他的 JSON 库,几乎不用编写代码,就可以处理 Case class / enum 的 JSON 映射。 (对比 spray-json,wjson 的 API 更简单、更强大)
  2. 强大的 interpolation 支持.
  3. JSON5 支持。JSON5 是 JSON 的超集,支持注释、多行字符串等特性,更适合于配置文件等场景。
  4. JSON Schema 支持,通过 ADT 类型生成 JSON Schema,提供更好的数据校验和提示。
  5. 当然,wjson 也是我学习 Scala 3 的一个练手项目,尤其是对 Scala3 的 Macro 的使用。 wjson 广泛的使用 Macro 来提供一个简单、强大的 API。

TODO List

  • JsValueMapper for Tuple

core

wjson 是一个Scala3 JSON库,它提供了一个简单的、直观的API,用于在Scala中处理JSON。

wJson的设计参考了如下的库,并试图变得提供更为 简单的API:

  1. Spray JSON
  2. wangzaixiang's fork of Spray JSON
  3. ujson

wjson-core 是 wjson 的核心模块,它提供了基本的 JSON 的核心数据结构,以及基本的 JSON 解析和序列化功能。

  • JsValue API: 对 JSON 数据结构的 ADT 抽象,支持 immutable 风格的 build, access, show 等操作,是 wjson 的核心数据结构。
  • JSON parer, 一个高性能的JSON解析器,支持标准的 JSON 语法
  • JSON5 parser, JSON5 语法解析器。JSON5 是 JSON 的超集,支持注释、多行字符串等特性。
  • json"..." 和 json5"..." 字符串插值
  • ADT 支持

安装

以 sbt 为例:

libraryDependencies += "com.github.wangzaixiang" %% "wjson-core" % "0.5.0-RC1"

使用

在使用 wjson 库之前,简单引入一下:

import wjson.{*, given}

basic JSON API

JSON 基本类型API

enum JsVal:
    case JsNull
    case JsBoolean(value: Boolean)
    case JsNumber(value: Double)
    case JsString(value: String)
    case JsArray(elements: List[JsVal])
    case JsObject(fields: Seq[String, JsVal])

wjson 提供的是基于 immutable 的 API

JsObject 会保留构建时字段的顺序,在格式化输出时,保留该顺序。但 equals 方法会忽略字段的顺序。

JSON对象

wjson 提供了多种方式来构造 JSON 值。

import wjson.{*, given}

// JSON Object: {"name":"John","age":18} 下面的多种方式,都可以构造出同一个 JSON 对象
val objStr = """{"name":"John","age":18}"""
val obj1 = JsObject(Seq("name" -> JsString("John"), "age" -> JsNumber(18))) // 使用 JsObject 构造器
val obj2 = JsObject(Seq("name" -> "John", "age" -> 18)) // 通过隐式转换,将基本类型转换为 JsValue
val obj3 = JsValue.obj("name" -> "John", "age" -> 10) // 使用 JsValue.obj 构造 JsObj, 更简洁

val obj4 = JsValue.parseJson(objStr)    // 作为 JSON 字符串解析
val obj5 = JsValue.parseJson5(objStr)   // 作为 JSON5 字符串解析

val obj6 = json"""{"name":"John","age":18}"""  // 使用 json"..." 字符串插值
val obj7 = json5"{name:'John', age:18 }"  // 使用 json5"..." 字符串插值, 注意 JSON5 的语法更宽松

// JSON Array: [1,2,3]
val arr1 = JsArray(Seq( JsNumber(1), JsNumber(2), JsNumber(3) )) // 最基本的构造方式
val arr2 = JsArray(Seq(1,2,3)) // 通过隐式转换,将基本类型转换为 JsValue
val arr3 = JsValue.arr(1,2,3) // 使用 JsValue.arr 构造 JsArray, 更简洁

val arr4 = JsValue.parseJson("[1,2,3]")    // 作为 JSON 字符串解析
val arr5 = JsValue.parseJson5("[1,2,3, ]")   // 作为 JSON5 字符串解析, JSON5 支持末尾的逗号

val arr6 = json"[1,2,3]"  // 使用 json"..." 字符串插值
val arr7 = json5"[1,2,3, ]"  // 使用 json5"..." 字符串插值, 注意 JSON5 的语法更宽松

基本 API

  1. 输出 JSON 字符串

      js1.show // 输出 JSON 字符串
      js1.showPretty // 输出格式化的 JSON 字符串
      js1.show(indent = 4) // 输出格式化的 JSON 字符串,缩进为4个空格
    
  2. JsObject 操作

      obj1.field("name")  // 类似于 Map 的 apply 方法,获取字段值
      obj1.filedOpt("name") // 类似于 Map 的 get 方法,获取字段值,返回 Option
      obj1.contains("name") // 判断是否包含某个字段
      obj1.keys // 获取所有的字段名
    
      obj1 ++ obj2 // 合并两个 JsObject, 如果有重复的字段,后者覆盖前者
      obj1 ++ Seq("name" -> "Tom", "age" -> 20) // 合并 JsObject 和 Map[String, Any]
      obj1 + ("name" -> "Tom") // 添加一个字段
    
      obj1.remove("field1", "field2") // 删除字段
    
  3. JsArray 操作

       arr1.elements // 获取所有的元素
       arr1(0) // 获取第一个元素
    
       arr1 ++ arr2 // 合并两个 JsArray
       arr1 ++ Seq(1,2,3) // 合并 JsArray 和 Seq[T]
       arr1 :+ 4    // 在尾部添加一个元素,返回新的 JsArray
       0 +: arr1    // 在头部添加一个元素,返回新的 JsArray
    
       arr1.updated(1, 100) // 更新第二个元素的值为 100,返回新的 JsArray
    
  4. JsValue 操作

       js1 match
         case JsNull => // JsNull 的模式匹配
         case JsBoolean(b) => b // JsBoolean 的模式匹配
         case JsNumber(n: Long) => n // JsNumber 的模式匹配
         case JsNumber(n: Double) => n // JsNumber 的模式匹配
         case JsString(s) => s // JsString 的模式匹配
         case JsObj(fields) => fields // JsObj 的模式匹配
         case JsArr(elements) => elements // JsArr 的模式匹配
    
       js1.isObj // 判断是否是 JsObj
       js1.isArr // 判断是否是 JsArr
       js1.isNull // 判断是否是 JsNull
       js1.isBoolean // 判断是否是 JsBoolean
       js1.isNumber // 判断是否是 JsNumber
       js1.isString // 判断是否是 JsString
    
       js1 == js2  // 判断是否相等, 对 JsObject, 会忽略字段的顺序
    
       js1.asBool // 强制转换为 JsBoolean,如果类型不匹配,会抛出异常
       js1.asNum // 强制转换为 JsNumber,如果类型不匹配,会抛出异常
       js1.asStr // 强制转换为 JsString,如果类型不匹配,会抛出异常
       js1.asObj // 强制转换为 JsObject,如果类型不匹配,会抛出异常
       js1.asArr // 强制转换为 JsArray,如果类型不匹配,会抛出异常
    
  5. JSON 与 对象值之间的转换

     case class Person(name: String, age: Int) derives JsValueMapper // 通过 derives JsValueMapper 自动生成 JSON 映射
     case class User(name: String, age: Int) // 未定义 JsValueMapper 也可以,但每次映射都会生成 JsValueMapper 实例, 会增加编译后的代码大小
     val js1 = json"""{"name":"John","age":18}"""
    
     val person = js1.toBean[Person]   // 反序列化为 Person
     val user = js1.toBean[User]       // 反序列化为 User
     val js2 = person.toJson           // 序列化为 JsValue
    
  6. 字符串插值

       val name = "John"
       val age = 18
       val obj1 = json"""{"name": $name, "age": $age}"""  // 使用 json"..." 字符串插值
       val obj2 = json5"{name: $name, age: $age }"  // 使用 json5"..." 字符串插值, 注意 JSON5 的语法更宽松
    
       val arr1 = json"[1,2,3, $age]"  // 使用 json"..." 字符串插值
       val arr2 = json5"[1,2,3, $age,  ]"  // 使用 json5"..." 字符串插值, 注意 JSON5 的语法更宽松
    

    字符串插值也可以用于进行简单的模式匹配:

       val js = json"{name: 'John', age: 18, }"
    
       jsval match
         case json"{name: $name, age: $age}" => println(name, age)  // John 18
         case _ => println("not match")
       
       json5"{ name: 'John', age: 18, address: { city: 'Beijing', country: 'China' }, scores: [80,90,100] }" match 
         case json5"{ address: {city: $city}, scores:[80,$score1,$score2]}" => println(city, score1, score2)  // Beijing 90 100
         case _ => println("not match")
       
    

    使用字符串插值进行模式匹配时,提供了一定的灵活性,例如:

    1. 对 JsObject, 在模式中可以仅匹配部分字段,而忽略其他字段。
    2. 对 JsArray, 可以匹配前面部分元素,而忽略其他元素。

    需要更强大的模式匹配功能,可以使用 wjson-pattern 模块。

JSON(JSON5) parser

wjson 提供了 2个 JSON Parser:

  1. 标准 JSON 解析器
  2. JSON5 解析器

Bean/JSON Mapping

wjson 提供了丰富的 Bean/JSON 映射支持:

    true.toJson  // JsBoolean(true)
    JsBoolean(true).to[Boolean]  // true

    123.toJson  // JsNumber(123)
    JsNumber(123).to[Int]  // 123

    "hello".toJson  // JsString("hello")
    JsString("hello").to[String]  // "hello"

    case class Person(name: String, age: Int)
    val p = Person("John", 18)
    p.toJson  // JsObject("name" -> JsString("John"), "age" -> JsNumber(18))
    json"""{ "name": "John", "age": 18 }""".to[Person]  // Person("John", 18)
  1. 基本类型的序列化,
    • Byte, Short, Int, Long, Float, Double, BigDecimal(scala, java), BigInt(scala, java)
    • Boolean
    • String
  2. Option[T]: 其中 T 必须是可序列化的(T: JsValueMapper)
  3. 集合类型
    • List[T]
    • Seq[T]
    • Set[T]
    • SortedSet[T]
    • Map[String, T]
    • Map[K, V] (K != String) 会映射为 List[(K,V)] 对应的 JSON 结构
    • Array[T] 集合元素的类型 T 必须是可序列化的(T: JsValueMapper)
  4. Tuple 类型, 目前尚未支持,计划在后续版本中支持
  5. case class: 其中所有的字段必须是可序列化的.
    1. 如果字段类型 T 是 Option[T1],则该字段是可选的,如果 JSON 中没有该字段,则使用 None 填充
    2. 如果字段有默认值,则该字段是可选的,如果 JSON 中没有该字段,则使用默认值填充。
    3. 所有其他的字段都是必须的。如果JSON 中没有该字段,则抛出异常。(不会对应于 null 值)
    4. case class 与 JSON 的映射关系定义,请参考:JSON Schema for ADT
  6. enum class: 其中所有的字段必须是可序列化的.
    1. enum class 与 JSON 的映射关系定义,请参考:JSON Schema for ADT

如果你的类型 T 不符合上述规则,但你又希望提供序列化、反序列化的能力,你可以为之提供一个 JsonValueMapper[T] 的隐式值。

  // support Map[K,V] mapping to List[(K,V)]
  given [K: JsValueMapper, V: JsValueMapper]: JsValueMapper[Map[K, V]] = mapMapping2[K, V]
  def mapMapping2[K: JsValueMapper, V: JsValueMapper]: JsValueMapper[Map[K, V]] = new JsValueMapper[Map[K, V]]:
    def fromJson(js: JsValue): Map[K, V] = (js: @unchecked) match
      case o: JsArray =>
        o.elements.map:
          case el: JsObject =>
            val key = summon[JsValueMapper[K]].fromJson(el.field("key"))  // expect key field
            val value = summon[JsValueMapper[V]].fromJson(el.field("value")) // expect value field
            key -> value
          case _ => throw new Exception(s"Expected JsObj but ${js.getClass}")
        .toMap

      case _ => throw new Exception(s"Expected JsObj but ${js.getClass}")

    def toJson(t: Map[K, V]): JsValue =
      val entry2Json = (k: K, v: V) => JsObject( "key" -> summon[JsValueMapper[K]].toJson(k), "value" -> summon[JsValueMapper[V]].toJson(v) )
      val entries = t.toList.map { case (k, v) => entry2Json(k,v) }
      JsArray( entries: _* )

wjson Pattern: 一个简单、直观的Scala JSON库

JSON作为数据交换标准,使用越来越广泛。对输入的 JSON 进行模式匹配、信息提取,并进行进一步的加工处理,是一个非常常见的场景。

应用场景:

Mock Server。在Mock Server中,我们需要配置大量的 Mock 规则,在 request 匹配规则的情况下,返回相应的数据。 当request/response 都可以使用JSON来表达的时候,我们就可以使用 JSON Pattern 来简单的描述一个 Mock Rule。

流量回放改写规则。在对录制流量进行回放时,我们需要配置一系列的改写规则,匹配某些条件的请求和返回的关键字段,被提取出来, 并用户改写后续请求的参数,以满足回放的需求。

使用脚本来进行JSON数据的处理。

测试工具。测试工具非常需要一个简单、高效的结果校验工具,对返回数据是JSON的场景,如果我们需要使用大量的字段提取、字段校验的话, 是非常枯燥的。而使用一个模式匹配工具,可以简化这一过程。

wjson 自定义了一个 JSON Pattern 语言,这个语言参考了:rejson 的设计,但进行了增强。 我们尽力保证这个语言的语法一致性,以让使用者尽可能快速的熟悉其使用方式。但客观来说,定义一个新的DSL, 并不是一件容器的事情,尤其是其对于使用者而言,是否有较低的认知成本(学习成本),还是存在很大的未知的。 (除非必要,不要轻易的创建DSL),因此,wjson-pattern 目前是一个实验性的库,我们会根据使用者的反馈,不断的改进。

  1. 简单示例

先看一个简单的 wjson pattern 示例:

// JSON data: curl https://api.github.com/repos/wangzaixiang/wjson/commits?per_page=1
{
"sha": "650e56cd380c311909cd50408bbb4884f1f5d21e",
"node_id": "C_kwDOHj94ltoAKDY1MGU1NmNkMzgwYzMxMTkwOWNkNTA0MDhiYmI0ODg0ZjFmNWQyMWU",
"commit": {
  "author": {
    "name": "wangzaixiang",
    "email": "949631531@qq.com",
    "date": "2022-07-05T14:23:09Z"
  },
  "committer": {
    "name": "wangzaixiang",
    "email": "949631531@qq.com",
    "date": "2022-07-05T14:23:09Z"
  },
  "message": "add taged string pattern support\nadd array index/filter support",
  "tree": {
    "sha": "89fbbf82e3c5ffd0e1a7978dd7778195a004df2c",
    "url": "https://api.github.com/repos/wangzaixiang/wjson/git/trees/89fbbf82e3c5ffd0e1a7978dd7778195a004df2c"
  },
  "url": "https://api.github.com/repos/wangzaixiang/wjson/git/commits/650e56cd380c311909cd50408bbb4884f1f5d21e",
  "comment_count": 0,
  "verification": {
    "verified": false,
    "reason": "unsigned",
    "signature": null,
    "payload": null
  }
},
"url": "https://api.github.com/repos/wangzaixiang/wjson/commits/650e56cd380c311909cd50408bbb4884f1f5d21e",
"html_url": "https://github.com/wangzaixiang/wjson/commit/650e56cd380c311909cd50408bbb4884f1f5d21e",
"comments_url": "https://api.github.com/repos/wangzaixiang/wjson/commits/650e56cd380c311909cd50408bbb4884f1f5d21e/comments",
"parents": [
  {
    "sha": "d90609ac4e7254eac5138453e2e07591a11bb55e",
    "url": "https://api.github.com/repos/wangzaixiang/wjson/commits/d90609ac4e7254eac5138453e2e07591a11bb55e",
    "html_url": "https://github.com/wangzaixiang/wjson/commit/d90609ac4e7254eac5138453e2e07591a11bb55e"
  }
]
}
// JSON Pattern
{
  sha: @sha,
  commit: { author: { name: @commit_name } }
  url: @url,
  parents/*/sha: @parents
}

对上述的示例 JSON,我们可以使用如下的 JSON pattern 来对其进行匹配,并完成相应字段的提取:

val info = "...json string..." 
  info.parseJson match
      case rejson"""
        {
          sha: $sha@_,
          commit: { author: { name: $commit_name@_ } },
          url: $url@_,
          parents/*/sha: $parents@_
        }
      """ =>
        println(s"sha = $sha, commit_name = $commit_name, url = $url, parents=$parents")
      
      case _ => println("not matched"

//
// sha = JsString(650e56cd380c311909cd50408bbb4884f1f5d21e), 
// commit_name = JsString(wangzaixiang), 
// url = JsString(https://api.github.com/repos/wangzaixiang/wjson/commits/650e56cd380c311909cd50408bbb4884f1f5d21e), 
// parents=JsArray(List(JsString(d90609ac4e7254eac5138453e2e07591a11bb55e)))

wjson pattern 语法说明

wjson pattern 是 JSON 语法的一个扩展:

  1. 在 # 之后的内容,是注释,不会影响匹配

  2. 支持基本类型:null, boolean(true, false), number, string('hello' or "hello")

         [ 1, 2.0, true, false, "hello", null ]
         'hello' # single quote string
    
  3. 支持复杂类型: array: [ ... ], object { name: value }。 在 object 中可以使用 类似于 JSON5 的语法,以增强可读性

      [ 1, 2, 3]                    # array
      { "name": "John", "age": 20 }
      { 'name': 'John', 'age': 20 }  # 与上一行等效
      { name: 'John', age: 20 }  # 与上一行等效
    
  4. 在所有可以是值的地方,可以使用 name@value 的语法,将value 的值绑定到name变量中返回。

     a @ 1  # 匹配 1 且将值绑定到 a 变量
     b @ [ 1,2,3 ] # 匹配 [1,2,3] 且将值绑定到 b 变量
     c @ { name: "John", age: 20 }  # 匹配 { name: "John", age: 20 } 且将值绑定到 c 变量
     [ 1, 2, x @ 3, 4 ]     # 匹配 [1,2,3,4] 且将 3绑定到 x 变量
     { name: n@string, age: a@number }  # 匹配 { name: "John", age: 20 } 且将 "John" 绑定到 n 变量,20 绑定到 a 变量
    
  5. 类型匹配: "boolean", "string", "number", "integer", "array", "object" 匹配对应的JS类型

    # input: { name: "John", age: 20, leader: true }
    { name: name @ string, age: age @ number, leader: leader @ boolean }
    # 匹配成功,并将 name 绑定到 "John", age 绑定到 20, leader 绑定到 true
    
  6. "_" 匹配任意单值

    [ 1, 2, _, 4]  #  match: [ 1,2,3,4], not match: [1,2,3,5,4]
    { name: _, age: 20 } # match { name: "John", age: 20 } but not { age: 20 }
    
  7. "_*" 在数组中,可以匹配 0 到多个数组成员,在对象中,可以匹配未被指定的所有其他字段。

    { name: "John", other@_* }
    # match { name: "Johe", age:20 , leader:true} and others := { age: 20, leader: true}
    [ 1,2, o@_*, 10 ] 
    # match [ 1,2,3,4,5,6,7,8,9,10] and o:=[3,4,5,6,7,8,9]
    
  8. 可以使用 tag"content"的方式扩展匹配规则,这个规则可以有用户来使用自定义的方式对值进行匹配。wjson中提供了 eval"expr" 和 r"expr" 两个参考实现,mvel使用MVEL表达式引擎来匹配一个JS值,r使用正则表达式来匹配值。

    { name: "John", age: eval"it >= 0 && it <= 100", email: r"\w+@(\w\.)*\w" }
    # match { name: "John", age: 20,  email: "john@qq.com" }
    
  9. 可以使用 a/b/c 的路径来匹配,等效于 { a : { b: {c : _ } } } 。

    { a/b/c: "John" } 
    # match { a: { b:  {c : "John" } } }
    
  10. 可以使用 /* 来匹配数组中的所有成员,等效于 [ _ ]

    { users/*/name: "John" }
    
  11. 可以使用 a[ pattern ] 或者 a[n] 的方式来对数组中的成员进行条件筛选(或选中第n个成员)。

    # { users: [ { name: "John", age: 20, email: "john@qq.com" },
    #            { name: "rose“, ageL 25, email: "rose@qq.com" } ]  }
    { users[ {name:"John"} ]/email: @email } 
    # 匹配成功,并绑定 变量 email 的值为 "john@qq.com"
    

JSON Schema 设计草案

Design Goals

  1. 使用 scala ADT 类型进行 JSON 建模,用于描述数据结果,可以自动生成 JSON Schema.
  2. 通过 @js.description 之类的 annotation 定义更多的 constraint.

Usage

  1. via macro during compile time

       val schema: String = JsonSchema.of[T]
       println(schema)
    
  2. running generator on tasty file

    //> using lib wjson.schema
    import wjson.schema.*
    
    JsonSchemaGenerator.generate("path/to/tasty/file.tasty", "path/to/output/schema.json")
    

Special JSON fields

  1. $schema: 对 JSON 元素,可以指定该元素的 JSON schema URI,通过该 URI 获取到 schema 信息,适合于:
    • top level JSON element
    • dynamic inner level JSON element 诸如 idea/visual code 之类的编辑器可以通过该 URI 获取到 schema 信息,从而提供更好的提示和校验。
  2. $version: 对应于 @js.version
  3. $enum, 为 SUM 类型生成的 tag,非 JSON Schema 标准字段
  4. $or, $value, 为 OR 类型生成的 tag,非 JSON Schema 标准字段

JSON Schema Annotations

可以在 ADT 申明中,使用如下的 annotation 附加JSON Schema的元信息。

  1. @js.description
  2. @js.enums
  3. @js.multipleOf
  4. @js.maximum
  5. @js.minimum
  6. @js.exclusiveMaximum
  7. @js.exclusiveMinimum
  8. @js.maxLength
  9. @js.minLength
  10. @js.pattern
  11. @js.maxItems
  12. @js.minItems
  13. @js.uniqueItems
  14. @js.maxContains
  15. @js.minContains
  16. @js.maxProperties
  17. @js.minProperties
  18. @js.format
    • date/time/datetime/duration
    • email
    • hostname
    • ipv4/ipv6
    • uri
    • uuid

扩展

  1. @js.toplevel

    表示改类型可以作为文档顶层元素,顶层元素在定义时,可选的支持 $schema 属性,用于指定 JSON Schema 的定义。

    tips: 可以在 JSON 文件中,手动指定 $schema, idea 编辑器会自动获取 schema 对文档进行校验 (不在需要额外配置 mapping)

  2. @js.version

  3. @js.open: mark a type additionalProperties= true

  4. @js.dynamic

ADT support

Primitive Types

Container Types

Product Type

wjson 支持 Product 类型的 JSON 序列化和反序列化。

      case class Bean(name: String, age: Int)

对应的 JSON 如下:

    {
      "name": "wang",
      "age": 18
    }

Sum Type

对 enum 类型,可以自动添加 $tag 字段,取 case 名作为值。

      enum Color:
         case Red, Green, Blue
         case Mixed(r: Int, g: Int, b: Int, alpha: Int)
     
  1. 对 simple case, 如 Red, Green, Blue,编码为 string,无需 tag
  2. 对 product case, 如 Mixed,编码为:
        {
          "$tag": "Mixed",
           "r": 10, "g": 20, "b": 30, "alpha": 255
        }
  • 读取更高版本的JSON时,可能会存在不认识的 tag, 会忽略该枚举值。

Or Type

wjson 支持 X | Y 的 Or Type 的 JSON 序列化和反序列化。

示例:

       case class Root(bean: Bean1 | List[Bean2] | Array[String]) 

对应的 JSON 如下:

    [{
       "bean":{
            "$or":"demo2.Bean1",
            "$value":{"name":"wang","age":18}
       }
    },
    {
      "bean":{
            "$or":"scala.collection.immutable.List[demo2.Bean2]",
            "$value":[ {"name":"zhang","age":20} ]
      }
    },
    {
      "bean": {
            "$or":"scala.Array[java.lang.String]",
            "$value":["hello","world"]
      }
    }]

Or Type 的成熟度不如 enum 的支持, 其规则相对比较复杂:

  1. T | Null: 理解为可以为 Null 的类型, 与 Option[T] 是类似的
  2. 如果有多个子类型,会生成一个 $or 的元素,指定其实际类型,使用 $value 作为值。
    • 基础子类型:Int | Long | String | Boolean | Double | Float | Null ,不会生成 tag
    • 对非基础类型, 取该类型的名称作为 tag
      1. 非容器类型(非范型)。 如 demo2.Bean1
      2. 对容器类型,如 scala.collection.immutable.List[demo2.Bean2]
      3. 需要注意,这里的 tag 与 scala 语言有绑定,未来考虑转化为一个语言中立的 tag。
    • 如果只有一个子类型需要 tag, 则会优化为不生成 $or
  3. 不能有 List[A] | List[B] 这样的类型,因为Java的擦除式范型,无法通过类型检测数据的实际类型。
    • 特殊支持: 可以支持 Option[A] | Option[B]
  4. Option[T]Null 不能同时存在。

使用 Or Type 时,因为要考虑的情况相对复杂,建议尽可能使用 enum 来替代。

JSON Pointer support

case class LocalPointer[T](p: String)  // ref inside the same document
case class GlobalPointer[T](p: String) // ref outside document

case class Relation(srcView: LocalPointer[View], destView: LocalPointer[View])

val relation = Relation("view1", "view2")
{ 
  "srcView": "view1", 
  "destView": "view2"
}

多版本支持

@js.version("1.0.6") case class Cube
(
   @js.since("1.0.5")
   name: Option[String],      // 这个元素是 1.0.5 版本开始引入的

   @js.deprecated("1.0.6")    // 在 1.0.6 这个版本中被废弃
   age: Option[Int],

   column: Column,    // 是一个枚举类型, 新版本,可以增加新的 枚举值
   dimension: Dimension // 是一个 Product 类型, 新版本,可以增加新的字段
)

场景:

  1. 新版本中,对某个 Product 类型增加新的字段
    • 该字段需要声明为 Option 类型,以兼容旧版本
    • 通过 @js.since("1.0.5") 来标记该字段是从 1.0.5 版本开始引入的
  2. 新版本中,对某个枚举类型增加新的枚举值
  3. 读取更新版本的数据时,由于当前版本中没有该字段(或枚举值),会自动的忽略该字段(或枚举值)
  4. 读取旧版本的数据时,会自动的填充默认值(对于 Option 类型,填充为 None)

Internal

wjson 设计文档。

wjson的设计依赖了 Scala 3 的 Macro

scala3 macro