scala-jsonschema icon indicating copy to clipboard operation
scala-jsonschema copied to clipboard

Schema for play-json JsValue

Open 0lejk4 opened this issue 3 years ago • 3 comments

Hello @andyglow I saw https://github.com/andyglow/scala-jsonschema#free-objects with info on how to create the schema for JsObject and now I am struggling with creating Schema for JsValue. Can you suggest the code for its schema? Also, have a question on how to have Schema for JsValue and its subtypes as because of Schema type-class covariance the behavior is strange, for example if I define schema for JsValue and JsNull, JsNull is used when Schema for JsValue is expected.

0lejk4 avatar Sep 27 '22 14:09 0lejk4

Hello, @0lejk4 There's not much we can infer from JsValue type as it's abstract.. At the compile time we don't know how you want to represent it, right? It can be anything. In json-schema would it would mean you need to use something like this:

"properties": {
  "my-abstract-prop": {}
}

Seems like this empty definition does the job. I'm not sure if you can do it with the library as original idea was to represent strongly typed information.

You can do something though, Please check this

import json.Schema, json.Json, json.schema.Predef
import com.github.andyglow.jsonschema._
import play.api.libs.json._

object PlayJsonBindings {

  implicit val playJsonNumberPredef: Predef[JsNumber] = Predef.bigDecimalS.asInstanceOf[Predef[JsNumber]]
  implicit val playJsonStringPredef: Predef[JsString] = Predef.strS.asInstanceOf[Predef[JsString]]
  implicit val playJsonBooleanPredef: Predef[JsBoolean] = Predef.boolS.asInstanceOf[Predef[JsBoolean]]


  case class Payload(
    id: String,
    num: JsNumber,
    str: JsString,
    bool: JsBoolean
  )
  
  def main(args: Array[String]): Unit = {
		val schema: Schema[Payload] = Json.schema[Payload]
    println(schema.draft07("foo"))
  }
}

https://scastie.scala-lang.org/andyglow/ehjsHlLaTNCScxihwnjdiQ/7

This shows that you can expose json-type information from the types that you are aware of.

May I ask you to describe your case little bit more detailed? Wonder why people would need to map json ast into schema.

andyglow avatar Oct 17 '22 23:10 andyglow

So the use case is having a field in DTO with some random JSON that we don't care about, but it then will be sent to someone who knows what to do with it. Initially, I had an idea as you sent with empty schema and to add a description that this is plain JSON. Another variant is to describe it like this:

{
  "$schema": "http://json-schema.org/draft-06/schema#",
  "type": "array",
  "items": {
    "$ref": "#/definitions/JsValue"
  },
  "definitions": {
    "JsObject": {
      "type": "object",
      "additionalProperties": {
        "$ref": "#/definitions/JsValue"
      },
      "title": "JsObject"
    },
    "JsValue": {
      "anyOf": [
        {
          "type": "array",
          "items": {
            "$ref": "#/definitions/JsValue"
          }
        },
        {
          "type": "boolean"
        },
        {
          "$ref": "#/definitions/JsObject"
        },
        {
          "type": "integer"
        },
        {
          "type": "null"
        },
        {
          "type": "string"
        }
      ],
      "title": "JsValue"
    }
  }
}

First is easier and enough for my use case but I still had issues with the covariance of the Schema typeclass. So I had schema for JsNull and JsValue, because of covariance they were colliding in implicit search, so I needed to use this trick but in the end in places where JsValue JsonSchema was expected I was getting JsNull`s schema.

0lejk4 avatar Oct 19 '22 10:10 0lejk4

you can also try something like this

import json.Schema, json.Json
import com.github.andyglow.jsonschema._

object PlayJsonBindings {

	sealed trait Val extends Any
  object Val {
    // Null case is not supported by scala-jsonschema library
    // as it's hard to imagine the straight use case 
    case class Str(x: String) extends AnyVal with Val
    case class Num(x: Double) extends AnyVal with Val
    case class Bool(x: Boolean) extends AnyVal with Val
    case class Arr(x: Seq[Val]) extends AnyVal with Val
    case class Dict(x: Map[String, Val]) extends AnyVal with Val
  }
  
  def main(args: Array[String]): Unit = {
		val schema: Schema[Val] = Json.schema[Val]
    println(schema.draft07("foo"))
  }
}

which is getting resulted in

{
  "$schema": "http://json-schema.org/draft-07/schema#",
  "$id": "foo",
  "$ref": "#PlayJsonBindings.Val",
  "definitions": {
    "PlayJsonBindings.Val": {
      "$id": "#PlayJsonBindings.Val",
      "oneOf": [
        {
          "type": "number"
        },
        {
          "type": "string"
        },
        {
          "type": "boolean"
        },
        {
          "type": "object",
          "patternProperties": {
            "^.*$": {
              "$ref": "#PlayJsonBindings.Val"
            }
          }
        },
        {
          "type": "array",
          "items": {
            "$ref": "#PlayJsonBindings.Val"
          }
        }
      ]
    }
  }
}

andyglow avatar Oct 19 '22 21:10 andyglow