From fd4909ff6e6cdba3b703721875eaa371808afb94 Mon Sep 17 00:00:00 2001 From: Ian Streeter Date: Wed, 31 Jul 2024 14:18:33 +0100 Subject: [PATCH] maxJsonDepth when validating JSON --- .../client/IgluCirceClient.scala | 29 ++- .../client/validator/CirceValidator.scala | 81 ++++---- .../jackson/snowplow/CirceToJsonError.scala | 39 ++++ .../io/circe/jackson/snowplow/package.scala | 109 ++++++----- .../SpecHelpers.scala | 4 +- .../validator/CachingValidationSpec.scala | 184 ++++++++++++++++-- 6 files changed, 343 insertions(+), 103 deletions(-) create mode 100644 modules/core/src/main/scala/io/circe/jackson/snowplow/CirceToJsonError.scala diff --git a/modules/core/src/main/scala/com.snowplowanalytics.iglu/client/IgluCirceClient.scala b/modules/core/src/main/scala/com.snowplowanalytics.iglu/client/IgluCirceClient.scala index 3d39cacf..3ae17707 100644 --- a/modules/core/src/main/scala/com.snowplowanalytics.iglu/client/IgluCirceClient.scala +++ b/modules/core/src/main/scala/com.snowplowanalytics.iglu/client/IgluCirceClient.scala @@ -36,8 +36,13 @@ import io.circe.{DecodingFailure, Json} */ final class IgluCirceClient[F[_]] private ( resolver: Resolver[F], - schemaEvaluationCache: SchemaEvaluationCache[F] + schemaEvaluationCache: SchemaEvaluationCache[F], + maxJsonDepth: Int ) { + @deprecated("Use `IgluCirceClient(resolver, cache, maxJsonDepth)`", "3.2.0") + def this(resolver: Resolver[F], cache: SchemaEvaluationCache[F]) = + this(resolver, cache, Int.MaxValue) + def check( instance: SelfDescribingData[Json] )(implicit @@ -50,7 +55,8 @@ final class IgluCirceClient[F[_]] private ( resolver.lookupSchemaResult(instance.schema, resolveSupersedingSchema = true) ) validation = - CirceValidator.WithCaching.validate(schemaEvaluationCache)(instance.data, resolverResult) + CirceValidator.WithCaching + .validate(schemaEvaluationCache)(instance.data, resolverResult, maxJsonDepth) _ <- EitherT(validation).leftMap(e => e.toClientError(resolverResult.value.supersededBy.map(_.asString)) ) @@ -61,21 +67,36 @@ final class IgluCirceClient[F[_]] private ( object IgluCirceClient { + @deprecated("Use `parseDefault(json, maxJsonDepth)`", "3.2.0") def parseDefault[F[_]: Monad: CreateResolverCache: InitValidatorCache]( json: Json + ): EitherT[F, DecodingFailure, IgluCirceClient[F]] = + parseDefault(json, Int.MaxValue) + + def parseDefault[F[_]: Monad: CreateResolverCache: InitValidatorCache]( + json: Json, + maxJsonDepth: Int ): EitherT[F, DecodingFailure, IgluCirceClient[F]] = for { config <- EitherT.fromEither[F](Resolver.parseConfig(json)) resolver <- Resolver.fromConfig[F](config) - client <- EitherT.liftF(fromResolver(resolver, config.cacheSize)) + client <- EitherT.liftF(fromResolver(resolver, config.cacheSize, maxJsonDepth)) } yield client + @deprecated("Use `fromResolver(resolver, cacheSize, maxJsonDepth)`", "3.2.0") def fromResolver[F[_]: Monad: InitValidatorCache]( resolver: Resolver[F], cacheSize: Int + ): F[IgluCirceClient[F]] = + fromResolver(resolver, cacheSize, Int.MaxValue) + + def fromResolver[F[_]: Monad: InitValidatorCache]( + resolver: Resolver[F], + cacheSize: Int, + maxJsonDepth: Int ): F[IgluCirceClient[F]] = { schemaEvaluationCache[F](cacheSize).map { cache => - new IgluCirceClient(resolver, cache) + new IgluCirceClient(resolver, cache, maxJsonDepth) } } diff --git a/modules/core/src/main/scala/com.snowplowanalytics.iglu/client/validator/CirceValidator.scala b/modules/core/src/main/scala/com.snowplowanalytics.iglu/client/validator/CirceValidator.scala index be19e390..0896c6e9 100644 --- a/modules/core/src/main/scala/com.snowplowanalytics.iglu/client/validator/CirceValidator.scala +++ b/modules/core/src/main/scala/com.snowplowanalytics.iglu/client/validator/CirceValidator.scala @@ -28,7 +28,7 @@ import scala.jdk.CollectionConverters._ // Cats import cats.Monad -import cats.data.{EitherNel, NonEmptyList} +import cats.data.NonEmptyList import cats.syntax.all._ // Jackson @@ -87,35 +87,37 @@ object CirceValidator extends Validator[Json] { private lazy val V4Schema = V4SchemaInstance.getSchema(new ObjectMapper().readTree(MetaSchemas.JsonSchemaV4Text)) - def validate(data: Json, schema: Json): Either[ValidatorError, Unit] = { - val jacksonJson = circeToJackson(schema) - evaluateSchema(jacksonJson) - .flatMap { schema => - validateOnReadySchema(schema, data).leftMap(ValidatorError.InvalidData.apply) - } - } - - def checkSchema(schema: Json): List[ValidatorError.SchemaIssue] = { - val jacksonJson = circeToJackson(schema) - validateSchemaAgainstV4(jacksonJson) + def validate(data: Json, schema: Json): Either[ValidatorError, Unit] = + for { + jacksonJson <- circeToJackson(schema, Int.MaxValue).leftMap(_.toInvalidSchema) + schema <- evaluateSchema(jacksonJson) + _ <- validateOnReadySchema(schema, data, Int.MaxValue) + } yield () + + @deprecated("Use `checkSchema(schema, maxJsonDepth)`", "3.2.0") + def checkSchema(schema: Json): List[ValidatorError.SchemaIssue] = + checkSchema(schema, Int.MaxValue) + + def checkSchema(schema: Json, maxJsonDepth: Int): List[ValidatorError.SchemaIssue] = { + circeToJackson(schema, maxJsonDepth) match { + case Left(e) => List(e.toSchemaIssue) + case Right(jacksonJson) => validateSchemaAgainstV4(jacksonJson) + } } /** Validate instance against schema and return same instance */ private def validateOnReadySchema( schema: JsonSchema, - instance: Json - ): EitherNel[ValidatorReport, Unit] = { - val messages = schema - .validate(circeToJackson(instance)) - .asScala - .toList - .map(fromValidationMessage) - - messages match { - case x :: xs => NonEmptyList(x, xs).asLeft - case Nil => ().asRight - } - } + instance: Json, + maxJsonDepth: Int + ): Either[ValidatorError.InvalidData, Unit] = + for { + jacksonJson <- circeToJackson(instance, maxJsonDepth).leftMap(_.toInvalidData) + _ <- schema.validate(jacksonJson).asScala.toList.map(fromValidationMessage) match { + case x :: xs => ValidatorError.InvalidData(NonEmptyList(x, xs)).asLeft + case Nil => ().asRight + } + } yield () private def fromValidationMessage(m: ValidationMessage): ValidatorReport = ValidatorReport(m.getMessage, m.getPath.some, m.getArguments.toList, m.getType.some) @@ -154,41 +156,48 @@ object CirceValidator extends Validator[Json] { def validate[F[_]: Monad]( schemaEvaluationCache: SchemaEvaluationCache[F] - )(data: Json, schema: SchemaLookupResult): F[Either[ValidatorError, Unit]] = { - getFromCacheOrEvaluate(schemaEvaluationCache)(schema) + )( + data: Json, + schema: SchemaLookupResult, + maxJsonDepth: Int + ): F[Either[ValidatorError, Unit]] = { + getFromCacheOrEvaluate(schemaEvaluationCache)(schema, maxJsonDepth) .map { _.flatMap { jsonschema => - validateOnReadySchema(jsonschema, data) - .leftMap(ValidatorError.InvalidData.apply) + validateOnReadySchema(jsonschema, data, maxJsonDepth) } } } private def getFromCacheOrEvaluate[F[_]: Monad]( evaluationCache: SchemaEvaluationCache[F] - )(result: SchemaLookupResult): F[Either[ValidatorError.InvalidSchema, JsonSchema]] = { + )( + result: SchemaLookupResult, + maxJsonDepth: Int + ): F[Either[ValidatorError.InvalidSchema, JsonSchema]] = { result match { case ResolverResult.Cached(key, SchemaItem(schema, _), timestamp) => evaluationCache.get((key, timestamp)).flatMap { case Some(alreadyEvaluatedSchema) => alreadyEvaluatedSchema.pure[F] case None => - provideNewJsonSchema(schema) + provideNewJsonSchema(schema, maxJsonDepth) .pure[F] .flatTap(result => evaluationCache.put((key, timestamp), result)) } case ResolverResult.NotCached(SchemaItem(schema, _)) => - provideNewJsonSchema(schema).pure[F] + provideNewJsonSchema(schema, maxJsonDepth).pure[F] } } private def provideNewJsonSchema( - schema: Json + schema: Json, + maxJsonDepth: Int ): Either[ValidatorError.InvalidSchema, JsonSchema] = { - val schemaAsNode = circeToJackson(schema) for { - _ <- validateSchema(schemaAsNode) - evaluated <- evaluateSchema(schemaAsNode) + schemaAsNode <- circeToJackson(schema, maxJsonDepth).leftMap(_.toInvalidSchema) + _ <- validateSchema(schemaAsNode) + evaluated <- evaluateSchema(schemaAsNode) } yield evaluated } diff --git a/modules/core/src/main/scala/io/circe/jackson/snowplow/CirceToJsonError.scala b/modules/core/src/main/scala/io/circe/jackson/snowplow/CirceToJsonError.scala new file mode 100644 index 00000000..f70edbaf --- /dev/null +++ b/modules/core/src/main/scala/io/circe/jackson/snowplow/CirceToJsonError.scala @@ -0,0 +1,39 @@ +/* + * Copyright (c) 2014-2024 Snowplow Analytics Ltd. All rights reserved. + * + * This program is licensed to you under the Apache License Version 2.0, + * and you may not use this file except in compliance with the Apache License Version 2.0. + * You may obtain a copy of the Apache License Version 2.0 at http://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Apache License Version 2.0 is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the Apache License Version 2.0 for the specific language governing permissions and limitations there under. + */ +package io.circe.jackson.snowplow + +import cats.data.NonEmptyList +import com.snowplowanalytics.iglu.client.validator.{ValidatorError, ValidatorReport} + +sealed trait CirceToJsonError extends Product with Serializable { + def message: String + + def toInvalidData: ValidatorError.InvalidData = + ValidatorError.InvalidData( + NonEmptyList.one( + ValidatorReport(message, Some("/"), List.empty, None) + ) + ) + + def toInvalidSchema: ValidatorError.InvalidSchema = + ValidatorError.InvalidSchema(NonEmptyList.one(toSchemaIssue)) + + def toSchemaIssue: ValidatorError.SchemaIssue = + ValidatorError.SchemaIssue(path = "/", message = message) +} + +object CirceToJsonError { + case object MaxDepthExceeded extends CirceToJsonError { + override def message: String = "Maximum allowed JSON depth exceeded" + } +} diff --git a/modules/core/src/main/scala/io/circe/jackson/snowplow/package.scala b/modules/core/src/main/scala/io/circe/jackson/snowplow/package.scala index 7ca7a2dd..49158bc2 100644 --- a/modules/core/src/main/scala/io/circe/jackson/snowplow/package.scala +++ b/modules/core/src/main/scala/io/circe/jackson/snowplow/package.scala @@ -13,6 +13,9 @@ package io.circe package jackson +import cats.syntax.either._ +import cats.syntax.traverse._ + import scala.jdk.CollectionConverters._ import java.math.{BigDecimal => JBigDecimal} @@ -30,60 +33,74 @@ package object snowplow { /** * Converts given circe's Json instance to Jackson's JsonNode * Numbers with exponents exceeding Integer.MAX_VALUE are converted to strings - * '''Warning: This implementation is not stack safe and will fail on very deep structures''' * @param json instance of circe's Json * @return converted JsonNode */ - final def circeToJackson(json: Json): JsonNode = - json.fold( - NullNode.instance, - BooleanNode.valueOf(_), - number => - if (json == negativeZeroJson) - DoubleNode.valueOf(number.toDouble) - else - number match { - case _: JsonBiggerDecimal | _: JsonBigDecimal => - number.toBigDecimal - .map(bigDecimal => { - if (bigDecimal.isValidInt) - IntNode.valueOf(bigDecimal.intValue) - else if (bigDecimal.isValidLong) { - LongNode.valueOf(bigDecimal.longValue) - } else if (bigDecimal.isWhole) { - BigIntegerNode.valueOf(bigDecimal.toBigInt.underlying) - } else - DecimalNode.valueOf(bigDecimal.underlying) - }) - .getOrElse(TextNode.valueOf(number.toString)) - case JsonLong(x) => LongNode.valueOf(x) - case JsonDouble(x) => DoubleNode.valueOf(x) - case JsonFloat(x) => FloatNode.valueOf(x) - case JsonDecimal(x) => - try - if (x.contains('.') || x.toLowerCase.contains('e')) - DecimalNode.valueOf(new JBigDecimal(x)) - else - getJsonNodeFromStringContent(x) - catch { - case _: NumberFormatException => TextNode.valueOf(x) - case _: JsonParseException => TextNode.valueOf(x) + final def circeToJackson(json: Json, maxJsonDepth: Int): Either[CirceToJsonError, JsonNode] = + if (maxJsonDepth <= 0) CirceToJsonError.MaxDepthExceeded.asLeft + else + json.fold( + NullNode.instance.asRight, + BooleanNode.valueOf(_).asRight, + number => + { + if (json == negativeZeroJson) + DoubleNode.valueOf(number.toDouble) + else + number match { + case _: JsonBiggerDecimal | _: JsonBigDecimal => + number.toBigDecimal + .map(bigDecimal => { + if (bigDecimal.isValidInt) + IntNode.valueOf(bigDecimal.intValue) + else if (bigDecimal.isValidLong) { + LongNode.valueOf(bigDecimal.longValue) + } else if (bigDecimal.isWhole) { + BigIntegerNode.valueOf(bigDecimal.toBigInt.underlying) + } else + DecimalNode.valueOf(bigDecimal.underlying) + }) + .getOrElse(TextNode.valueOf(number.toString)) + case JsonLong(x) => LongNode.valueOf(x) + case JsonDouble(x) => DoubleNode.valueOf(x) + case JsonFloat(x) => FloatNode.valueOf(x) + case JsonDecimal(x) => + try + if (x.contains('.') || x.toLowerCase.contains('e')) + DecimalNode.valueOf(new JBigDecimal(x)) + else + getJsonNodeFromStringContent(x) + catch { + case _: NumberFormatException => TextNode.valueOf(x) + case _: JsonParseException => TextNode.valueOf(x) + } } - }, - s => TextNode.valueOf(s), - array => JsonNodeFactory.instance.arrayNode.addAll(array.map(circeToJackson).asJava), - obj => - objectNodeSetAll( - JsonNodeFactory.instance.objectNode, - obj.toMap.map { case (k, v) => - (k, circeToJackson(v)) - }.asJava - ) - ) + }.asRight, + s => TextNode.valueOf(s).asRight, + array => + array + .traverse(circeToJackson(_, maxJsonDepth - 1)) + .map(l => JsonNodeFactory.instance.arrayNode.addAll(l.asJava)), + obj => + obj.toList + .traverse { case (k, v) => + circeToJackson(v, maxJsonDepth - 1).map((k, _)) + } + .map { l => + objectNodeSetAll( + JsonNodeFactory.instance.objectNode, + l.toMap.asJava + ) + } + ) def objectNodeSetAll(node: ObjectNode, fields: java.util.Map[String, JsonNode]): JsonNode = node.setAll[JsonNode](fields) private def getJsonNodeFromStringContent(content: String): JsonNode = mapper.readTree(content) + + @deprecated("Use `circeToJackson(json, maxJsonDepth)`", "3.2.0") + final def circeToJackson(json: Json): JsonNode = + circeToJackson(json, Int.MaxValue).toOption.get } diff --git a/modules/core/src/test/scala/com.snowplowanalytics.iglu.client/SpecHelpers.scala b/modules/core/src/test/scala/com.snowplowanalytics.iglu.client/SpecHelpers.scala index a5d906d6..cedfb8c2 100644 --- a/modules/core/src/test/scala/com.snowplowanalytics.iglu.client/SpecHelpers.scala +++ b/modules/core/src/test/scala/com.snowplowanalytics.iglu.client/SpecHelpers.scala @@ -108,5 +108,7 @@ object SpecHelpers { val TestResolver = Resolver.init[IO](10, None, EmbeddedTest) val TestClient = for { resolver <- TestResolver } yield Client(resolver, CirceValidator) val CachingTestClient = - TestResolver.flatMap(resolver => IgluCirceClient.fromResolver[IO](resolver, cacheSize = 10)) + TestResolver.flatMap(resolver => + IgluCirceClient.fromResolver[IO](resolver, cacheSize = 10, maxJsonDepth = 40) + ) } diff --git a/modules/core/src/test/scala/com.snowplowanalytics.iglu.client/validator/CachingValidationSpec.scala b/modules/core/src/test/scala/com.snowplowanalytics.iglu.client/validator/CachingValidationSpec.scala index 719378f2..53d836f1 100644 --- a/modules/core/src/test/scala/com.snowplowanalytics.iglu.client/validator/CachingValidationSpec.scala +++ b/modules/core/src/test/scala/com.snowplowanalytics.iglu.client/validator/CachingValidationSpec.scala @@ -35,6 +35,8 @@ import org.specs2.Specification import scala.concurrent.duration.DurationInt class CachingValidationSpec extends Specification { + val DefaultMaxJsonDepth = 40 + def is = s2""" This is a specification to test the basic caching Validatable functionality @@ -52,6 +54,10 @@ class CachingValidationSpec extends Specification { validation error for V4 non-compliant schema $e11 cache parsed json schemas $e12 not cache parsed json schemas $e13 + return error with the schema that exceeds maximum allowed JSON depth $e14 + return error with the JSON instance that exceeds maximum allowed JSON depth $e15 + return empty from checkSchema if schema has no issue $e16 + return errors from checkSchema if schema exceeds maximum allowed JSON depth $e17 """ val simpleSchemaResult: Json = @@ -87,7 +93,8 @@ class CachingValidationSpec extends Specification { CirceValidator.WithCaching .validate(createCache())( json, - ResolverResult.NotCached(SchemaItem(simpleSchemaResult, None)) + ResolverResult.NotCached(SchemaItem(simpleSchemaResult, None)), + DefaultMaxJsonDepth ) result must beRight @@ -150,7 +157,8 @@ class CachingValidationSpec extends Specification { CirceValidator.WithCaching .validate(createCache())( nonStringInput, - ResolverResult.NotCached(SchemaItem(simpleSchemaResult, None)) + ResolverResult.NotCached(SchemaItem(simpleSchemaResult, None)), + DefaultMaxJsonDepth ) must beLeft( nonStringExpected ) @@ -158,7 +166,8 @@ class CachingValidationSpec extends Specification { CirceValidator.WithCaching .validate(createCache())( missingKeyInput, - ResolverResult.NotCached(SchemaItem(simpleSchemaResult, None)) + ResolverResult.NotCached(SchemaItem(simpleSchemaResult, None)), + DefaultMaxJsonDepth ) must beLeft( missingKeyExpected ) @@ -166,7 +175,8 @@ class CachingValidationSpec extends Specification { CirceValidator.WithCaching .validate(createCache())( heterogeneusArrayInput, - ResolverResult.NotCached(SchemaItem(simpleSchemaResult, None)) + ResolverResult.NotCached(SchemaItem(simpleSchemaResult, None)), + DefaultMaxJsonDepth ) must beLeft( heterogeneusArrayExpected ) @@ -174,7 +184,8 @@ class CachingValidationSpec extends Specification { CirceValidator.WithCaching .validate(createCache())( doubleErrorInput, - ResolverResult.NotCached(SchemaItem(simpleSchemaResult, None)) + ResolverResult.NotCached(SchemaItem(simpleSchemaResult, None)), + DefaultMaxJsonDepth ) must beLeft( doubleErrorExpected ) @@ -205,7 +216,8 @@ class CachingValidationSpec extends Specification { CirceValidator.WithCaching .validate(createCache())( input, - ResolverResult.NotCached(SchemaItem(schema, None)) + ResolverResult.NotCached(SchemaItem(schema, None)), + DefaultMaxJsonDepth ) must beLeft( expected ) @@ -224,7 +236,8 @@ class CachingValidationSpec extends Specification { CirceValidator.WithCaching .validate(createCache())( input, - ResolverResult.NotCached(SchemaItem(schema, None)) + ResolverResult.NotCached(SchemaItem(schema, None)), + DefaultMaxJsonDepth ) must beRight } @@ -257,7 +270,8 @@ class CachingValidationSpec extends Specification { CirceValidator.WithCaching .validate(createCache())( input, - ResolverResult.NotCached(SchemaItem(schema, None)) + ResolverResult.NotCached(SchemaItem(schema, None)), + DefaultMaxJsonDepth ) must beLeft( expected ) @@ -289,7 +303,8 @@ class CachingValidationSpec extends Specification { CirceValidator.WithCaching .validate(createCache())( input, - ResolverResult.NotCached(SchemaItem(schema, None)) + ResolverResult.NotCached(SchemaItem(schema, None)), + DefaultMaxJsonDepth ) must beLeft( expected ) @@ -301,7 +316,8 @@ class CachingValidationSpec extends Specification { CirceValidator.WithCaching .validate(createCache())( input, - ResolverResult.NotCached(SchemaItem(schema, None)) + ResolverResult.NotCached(SchemaItem(schema, None)), + DefaultMaxJsonDepth ) must beRight } @@ -311,7 +327,8 @@ class CachingValidationSpec extends Specification { CirceValidator.WithCaching .validate(createCache())( input, - ResolverResult.NotCached(SchemaItem(schema, None)) + ResolverResult.NotCached(SchemaItem(schema, None)), + DefaultMaxJsonDepth ) must beRight } @@ -321,7 +338,8 @@ class CachingValidationSpec extends Specification { CirceValidator.WithCaching .validate(createCache())( input, - ResolverResult.NotCached(SchemaItem(schema, None)) + ResolverResult.NotCached(SchemaItem(schema, None)), + DefaultMaxJsonDepth ) must beLeft } @@ -331,7 +349,8 @@ class CachingValidationSpec extends Specification { CirceValidator.WithCaching .validate(createCache())( input, - ResolverResult.NotCached(SchemaItem(schema, None)) + ResolverResult.NotCached(SchemaItem(schema, None)), + DefaultMaxJsonDepth ) must beRight } @@ -351,7 +370,8 @@ class CachingValidationSpec extends Specification { CirceValidator.WithCaching .validate(createCache())( input, - ResolverResult.NotCached(SchemaItem(schema, None)) + ResolverResult.NotCached(SchemaItem(schema, None)), + DefaultMaxJsonDepth ) must beLeft( expected ) @@ -368,7 +388,8 @@ class CachingValidationSpec extends Specification { CirceValidator.WithCaching .validate(cache)( input, - ResolverResult.Cached(schemaKey, SchemaItem(schema, None), timestamp = 1.seconds) + ResolverResult.Cached(schemaKey, SchemaItem(schema, None), timestamp = 1.seconds), + DefaultMaxJsonDepth ) result must beRight(()) and @@ -382,12 +403,143 @@ class CachingValidationSpec extends Specification { val schema = json"""{ "type": "number" }""" val input = json"""5""" val result = CirceValidator.WithCaching - .validate(cache)(input, ResolverResult.NotCached(SchemaItem(schema, None))) + .validate(cache)( + input, + ResolverResult.NotCached(SchemaItem(schema, None)), + DefaultMaxJsonDepth + ) result must beRight(()) and (cache.get((schemaKey, 1.seconds)) must beNone) } + def e14 = { + val schema = + json"""{ + "type": "object", + "properties": { + "example_field": { + "type": "array", + "description": "the example_field is a collection of user names", + "users": { + "type": "object", + "properties": { + "name": { + "type": "string", + "maxLength": 128 + } + }, + "required": [ + "id" + ], + "additionalProperties": false + } + } + } + }""" + val input = json"""5""" + val expected = ValidatorError.InvalidSchema( + NonEmptyList.of( + SchemaIssue( + "/", + "Maximum allowed JSON depth exceeded" + ) + ) + ) + + CirceValidator.WithCaching + .validate(createCache())( + input, + ResolverResult.NotCached(SchemaItem(schema, None)), + maxJsonDepth = 5 + ) must beLeft(expected) + } + + def e15 = { + val schema = json"""{ "type": "number" }""" + val input = + json"""{"d1":{"d2":{"d3":{"d4":{"d5":{"d6":6}}}}}}""" + val expected = ValidatorError.InvalidData( + NonEmptyList.of( + ValidatorReport( + "Maximum allowed JSON depth exceeded", + Some("/"), + List.empty, + None + ) + ) + ) + + CirceValidator.WithCaching + .validate(createCache())( + input, + ResolverResult.NotCached(SchemaItem(schema, None)), + maxJsonDepth = 5 + ) must beLeft(expected) + } + + def e16 = { + val schema = + json"""{ + "type": "object", + "properties": { + "example_field": { + "type": "array", + "description": "the example_field is a collection of user names", + "users": { + "type": "object", + "properties": { + "name": { + "type": "string", + "maxLength": 128 + } + }, + "required": [ + "id" + ], + "additionalProperties": false + } + } + } + }""" + + CirceValidator.checkSchema(schema, maxJsonDepth = 10) must beEmpty + } + + def e17 = { + val schema = + json"""{ + "type": "object", + "properties": { + "example_field": { + "type": "array", + "description": "the example_field is a collection of user names", + "users": { + "type": "object", + "properties": { + "name": { + "type": "string", + "maxLength": 128 + } + }, + "required": [ + "id" + ], + "additionalProperties": false + } + } + } + }""" + val expected = List( + SchemaIssue( + "/", + "Maximum allowed JSON depth exceeded" + ) + ) + + CirceValidator.checkSchema(schema, maxJsonDepth = 5) must beEqualTo(expected) + } + private def createCache() = CreateLruMap[Id, SchemaEvaluationKey, SchemaEvaluationResult].create(10) }