Skip to content

Instantly share code, notes, and snippets.

@chuwy
Created February 23, 2017 12:57
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save chuwy/767bd516a8f7755d4d9bd7699690346d to your computer and use it in GitHub Desktop.
Save chuwy/767bd516a8f7755d4d9bd7699690346d to your computer and use it in GitHub Desktop.
Json Schema Validator
package jsonschemavalidator
import scala.language.implicitConversions
import cats.syntax.either._
import cats.syntax.semigroup._
import cats.syntax.semigroupk._
import cats.syntax.validated._
import cats.data.ValidatedNel
import cats.instances.all._
import io.circe.{Json, JsonNumber}
import io.circe.parser.parse
import org.json4s.{JArray, JBool, JDecimal, JDouble, JInt, JLong, JNothing, JNull, JObject, JString, JValue}
import com.snowplowanalytics.iglu.schemaddl.jsonschema.{CommonProperties, JsonSchemaProperty, NumberProperties, Schema}
import com.snowplowanalytics.iglu.schemaddl.jsonschema.json4s.Json4sToSchema._
object Validator {
def jbool(json: Json): Option[JValue] = json.asBoolean.map(JBool.apply)
def jnull(json: Json): Option[JValue] = if (json.isNull) Some(JNull) else None
def jarray(json: Json): Option[JValue] = json.asArray.map(a => JArray(a.map(circeTo4).toList))
def jobject(json: Json): Option[JValue] = json.asObject.map { o => JObject(o.toMap.mapValues(circeTo4).toList) }
def jnumber(json: Json): Option[JValue] = json.asNumber.flatMap(jnumber)
def jstring(json: Json): Option[JValue] = json.asString.map(JString.apply)
def jnumber(number: JsonNumber): Option[JValue] =
number.toBigInt.map(i => JInt(i): JValue) <+> Option(JDouble(number.toDouble))
implicit def circeTo4(json: Json): JValue = {
val j = jstring(json) <+> jobject(json) <+> jnumber(json) <+> jbool(json) <+> jarray(json) <+> jnull(json)
j.getOrElse(JNull)
}
implicit def jvalueToCirce(jValue: JValue): Json = jValue match {
case JString(s) => Json.fromString(s)
case JObject(vals) => Json.fromFields(vals.map { case (k, v) => (k, jvalueToCirce(v))})
case JInt(i) => Json.fromBigInt(i)
case JDouble(d) => Json.fromDoubleOrNull(d)
case JBool(b) => Json.fromBoolean(b)
case JArray(arr) => Json.arr(arr.map(jvalueToCirce): _*)
case JNull => Json.Null
case JDecimal(d) => Json.fromBigDecimal(d)
case JLong(l) => Json.fromLong(l)
case JNothing => Json.Null
}
/**
* `JsonSchemaProperty` is not-sealed trait, anyone can extend it
* So, anyone can add new property, which is sensible
* In that case he'll also need to maintain validation for it, which is sensible
*/
sealed trait PropertyInvalidation[+P <: JsonSchemaProperty]
case class TypeInvalidation(actual: CommonProperties.Type, expected: CommonProperties.Type) extends PropertyInvalidation[CommonProperties.Type]
case class MinInvalidation(actual: JsonNumber, expected: NumberProperties.Minimum) extends PropertyInvalidation[NumberProperties.Minimum]
type SchemaValidation[P <: JsonSchemaProperty] =
ValidatedNel[PropertyInvalidation[P], Unit]
val succ: SchemaValidation[Nothing] =
().validNel
trait Property[P <: JsonSchemaProperty] {
def validate(p: P, json: Json): SchemaValidation[P]
}
implicit object MinProperty extends Property[NumberProperties.Minimum] {
def validate(property: NumberProperties.Minimum, json: Json) = {
json.asNumber match {
case Some(actual) => actual.toBigInt match {
case Some(actualBI) if actualBI < property.getAsDecimal.toBigInt() => MinInvalidation(actual, property).invalidNel
case None if actual.toDouble < property.getAsDecimal => MinInvalidation(actual, property).invalidNel
case _ => succ
}
case None => succ
}
}
}
implicit object TypeProperty extends Property[CommonProperties.Type] {
private def getType(json: Json): CommonProperties.Type = json match {
case _ if json.isBoolean => CommonProperties.Boolean
case _ if json.isString => CommonProperties.String
case _ if json.isObject => CommonProperties.Object
case _ if json.isArray => CommonProperties.Array
case _ if json.isNull => CommonProperties.Null
case _ if json.isNumber => json.asNumber match {
case Some(n) if n.toLong.isDefined => CommonProperties.Integer
case _ => CommonProperties.Number
}
}
def validate(typeProperty: CommonProperties.Type, json: Json): SchemaValidation[CommonProperties.Type] = {
val `type` = getType(json)
typeProperty match {
case CommonProperties.Product(l) if l.contains(`type`) => succ
case _ if `type` == typeProperty => succ
case _ => TypeInvalidation(`type`, typeProperty).invalidNel
}
}
}
implicit class PropertyOps[+P <: JsonSchemaProperty: Property](property: P) {
def validate(json: Json): SchemaValidation[P] = {
implicitly[Property[P]].validate(property, json)
}
}
implicit class PropertyOptionOps[+P <: JsonSchemaProperty: Property](property: Option[P]) {
def validate(json: Json): SchemaValidation[P] = property match {
case Some(p) => p.validate(json)
case None => succ
}
}
def validateJson(schema: Schema, json: Json) = {
val t: SchemaValidation[JsonSchemaProperty] =
schema.`type`.validate(json)
val m: SchemaValidation[JsonSchemaProperty] =
schema.minimum.validate(json)
t |+| m
}
def main(args: Array[String]): Unit = {
val schema =
"""
|{"type": ["string", "null"], "minimum": 33}
""".stripMargin
val jsonSchema: JValue = parse(schema).toOption.get
val schemaO = Schema.parse(jsonSchema).get
val js = Json.fromString("ff")
val js2 = Json.fromInt(32)
println(validateJson(schemaO, js))
println(validateJson(schemaO, js2))
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment