All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.mesosphere.cosmos.jsonschema.JsonSchema.scala Maven / Gradle / Ivy

package com.mesosphere.cosmos.jsonschema

import cats.data.Xor
import com.fasterxml.jackson.databind.JsonNode
import com.fasterxml.jackson.databind.node.JsonNodeFactory
import com.github.fge.jsonschema.main.{JsonSchemaFactory, JsonValidator}
import io.circe.jawn.parse
import io.circe.syntax._
import io.circe.{Decoder, Encoder, Json, JsonObject}

import scala.collection.JavaConverters._

object JsonSchema {

  import Jackson._

  type ValidationErrors = Iterable[Json]

  /**
    * Validates `document` against `schema` returning all validation failures.
    * @param document   The document to validation
    * @param schema     The schema to validate against
    * @param jsf        The configured factory used to acquire the validator used for the validation.
    * @return           Returns an [[cats.data.Xor Xor]] representing the result of validating `document` against `schema`.
    *                   [[cats.data.Xor.Left Xor.Left[ValidationErrors] ]] Will be returned containing all validation
    *                   failures if they occur. [[cats.data.Xor.Right Xor.Right[Unit] ]] Will be returned if no validation failures occur.
    */
  def jsonObjectMatchesSchema(
    document: JsonObject,
    schema: JsonObject
  )(
    implicit jsf: JsonSchemaFactory
  ): Xor[ValidationErrors, Unit] = {
    jsonMatchesSchema(Json.fromJsonObject(document), Json.fromJsonObject(schema))
  }

  /**
    * Validates `document` against `schema` returning all validation failures.
    * @param document   The document to validation
    * @param schema     The schema to validate against
    * @param jsf        The configured factory used to acquire the validator used for the validation.
    * @return           Returns an [[cats.data.Xor Xor]] representing the result of validating `document` against `schema`.
    *                   [[cats.data.Xor.Left Xor.Left[ValidationErrors] ]] Will be returned containing all validation
    *                   failures if they occur. [[cats.data.Xor.Right Xor.Right[Unit] ]] Will be returned if no validation failures occur.
    */
  def jsonMatchesSchema(
    document: Json,
    schema: Json
  )(
    implicit jsf: JsonSchemaFactory
  ): Xor[Iterable[Json], Unit] = {
    val Xor.Right(documentNode) = document.as[JsonNode]
    val Xor.Right(schemaNode) = schema.as[JsonNode]

    val validationErrors = jsf.getValidator.validate(schemaNode, documentNode)
    if (validationErrors.isSuccess) {
      Xor.right[ValidationErrors, Unit](())
    } else {
      Xor.Left(
        validationErrors
          .asScala
          .map { message =>
            val jacksonJson = message.asJson
            val circeJson = jacksonJson.asJson
            circeJson
          }
      )
    }
  }

  /**
    * Traverse a json schema `schema` and create a document representing all the default property values defined in the
    * schema. If the schema has nested properties, the nesting will be preserved.
    * @param schema The schema to extract default property values from
    * @return       A document representing each property with its corresponding default value
    */
  def extractDefaultsFromSchema(schema: JsonObject): JsonObject = {
    val props = schema("properties").getOrElse(Json.Null)

    filterDefaults(props)
      .asObject
      .getOrElse(JsonObject.empty)
  }

  private[this] def filterDefaults(properties: Json): Json = {
    val defaults = properties
      .asObject
      .getOrElse(JsonObject.empty)
      .toMap
      .flatMap { case (propertyName, propertyJson) =>
        propertyJson
          .asObject
          .flatMap { propertyObject =>
            propertyObject("default").orElse {
              propertyObject("properties").map(filterDefaults)
            }
          }
          .map(propertyName -> _)
      }

    Json.fromJsonObject(JsonObject.fromMap(defaults))
  }

  /**
    * The validation library we use to validate the schema operates on the Jackson AST for the json document.
    * This object has a set of utilities to convert between circe and jackson.
    */
  private[this] object Jackson {

    implicit val JsonNodeEncoder: Encoder[JsonNode] = Encoder.instance(jsonNodeToCirceJson)

    implicit val JsonNodeDecoder: Decoder[JsonNode] = Decoder.instance { hcursor =>
      Xor.Right(circeJsonToJsonNode(hcursor.top))
    }

    private[this] def jsonNodeToCirceJson(node: JsonNode): Json = {
      // Inefficient, but good enough for now
      val Xor.Right(json) = parse(node.toString)
      json
    }

    private[this] def circeJsonToJsonNode(json: Json): JsonNode = {
      json.fold(
        jsonNull = JsonNodeFactory.instance.nullNode,
        jsonBoolean = JsonNodeFactory.instance.booleanNode,
        jsonNumber = { jsonNumber =>
          (jsonNumber.toBigInt, jsonNumber.toBigDecimal) match {
            case (Some(bigInt), _) => JsonNodeFactory.instance.numberNode(bigInt.underlying())
            case (_, Some(bigDec)) => JsonNodeFactory.instance.numberNode(bigDec.underlying())
            case _ => throw new NumberFormatException
          }
        },
        jsonString = JsonNodeFactory.instance.textNode,
        jsonArray = { arr =>
          val arrayNode = JsonNodeFactory.instance.arrayNode()
          arrayNode.addAll(arr.map(circeJsonToJsonNode).asJava)
        },
        jsonObject = { obj =>
          val objectNode = JsonNodeFactory.instance.objectNode()
          objectNode.setAll(obj.toMap.mapValues(circeJsonToJsonNode).asJava)
        }
      )
    }

  }

}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy