Created
February 23, 2017 12:57
-
-
Save chuwy/767bd516a8f7755d4d9bd7699690346d to your computer and use it in GitHub Desktop.
Json Schema Validator
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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