Skip to content

Commit 0e5fccd

Browse files
committed
Fix JSON Schema generation
1 parent bd8f8cf commit 0e5fccd

8 files changed

Lines changed: 223 additions & 126 deletions

File tree

schema/shared/src/main/scala/zio/blocks/schema/json/JsonCodec.scala

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616

1717
package zio.blocks.schema.json
1818

19-
import zio.blocks.chunk.{Chunk, ChunkBuilder, ChunkMap, NonEmptyChunk}
19+
import zio.blocks.chunk.{Chunk, ChunkBuilder, NonEmptyChunk}
2020
import zio.blocks.schema.SchemaError.ExpectationMismatch
2121
import zio.blocks.schema.{DynamicOptic, DynamicValue, PrimitiveValue, SchemaError}
2222
import zio.blocks.schema.binding.Registers
@@ -567,10 +567,7 @@ object JsonCodec {
567567

568568
override def encodeValue(x: Unit): Json = Json.Object.empty
569569

570-
override val toJsonSchema: JsonSchema = JsonSchema.obj(
571-
properties = new Some(ChunkMap.empty),
572-
additionalProperties = new Some(JsonSchema.False)
573-
)
570+
override val toJsonSchema: JsonSchema = JsonSchema.obj(maxProperties = NonNegativeInt(0))
574571
}
575572
val booleanCodec: JsonCodec[Boolean] = new JsonCodec[Boolean] {
576573
def decodeValue(in: JsonReader): Boolean = in.readBoolean()

schema/shared/src/main/scala/zio/blocks/schema/json/JsonCodecDeriver.scala

Lines changed: 168 additions & 73 deletions
Large diffs are not rendered by default.

schema/shared/src/main/scala/zio/blocks/schema/json/JsonSchema.scala

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -533,9 +533,7 @@ sealed trait JsonSchema extends Product with Serializable {
533533

534534
/** Make this schema nullable (accepts null in addition to current types). */
535535
def withNullable: JsonSchema = this match {
536-
case _: JsonSchema.True.type => JsonSchema.True
537-
case _: JsonSchema.False.type => JsonSchema.ofType(JsonSchemaType.Null)
538-
case s: JsonSchema.Object =>
536+
case s: JsonSchema.Object =>
539537
s.`type` match {
540538
case Some(st) =>
541539
st match {
@@ -551,6 +549,21 @@ sealed trait JsonSchema extends Product with Serializable {
551549
case _ =>
552550
new JsonSchema.Object(anyOf = new Some(NonEmptyChunk(JsonSchema.ofType(JsonSchemaType.Null), s)))
553551
}
552+
case _: JsonSchema.False.type => JsonSchema.ofType(JsonSchemaType.Null)
553+
case _ => this
554+
}
555+
556+
/** Add a required property for the provided discriminator field. */
557+
def withDiscriminatorField(name: String, value: String): JsonSchema = this match {
558+
case s: JsonSchema.Object =>
559+
val valueSchema = new JsonSchema.Object(const = new Some(new Json.String(value)))
560+
s.properties match {
561+
case Some(m: ChunkMap[String @unchecked, JsonSchema @unchecked]) =>
562+
s.copy(properties = new Some(ChunkMap.fromChunks(name +: m.keysChunk, valueSchema +: m.valuesChunk)))
563+
case _ =>
564+
s.copy(properties = new Some(ChunkMap.fromChunks(Chunk.single(name), Chunk.single(valueSchema))))
565+
}
566+
case _ => this
554567
}
555568
}
556569

schema/shared/src/main/scala/zio/blocks/schema/json/JsonSchemaToReflect.scala

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -82,11 +82,11 @@ private[schema] object JsonSchemaToReflect {
8282
case _: JsonSchema.False.type => Shape.Dynamic
8383
case obj: JsonSchema.Object =>
8484
analyzeEnum(obj)
85+
.orElse(analyzeSequence(obj))
86+
.orElse(analyzeMapOrRecord(obj))
8587
.orElse(analyzeOption(obj))
8688
.orElse(analyzeVariant(obj))
8789
.orElse(analyzeTuple(obj))
88-
.orElse(analyzeSequence(obj))
89-
.orElse(analyzeMapOrRecord(obj))
9090
.orElse(analyzePrimitive(obj))
9191
.getOrElse(Shape.Dynamic)
9292
}

schema/shared/src/test/scala/zio/blocks/schema/json/JsonCodecDeriverSpec.scala

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1703,7 +1703,7 @@ object JsonCodecDeriverSpec extends SchemaBaseSpec {
17031703
def encodeValue(x: BigInt, out: JsonWriter): Unit = out.writeValAsString(x)
17041704

17051705
override def decodeValue(json: Json): BigInt = json match {
1706-
case s: Json.String => BigInt(s.value)
1706+
case s: Json.String => BigInt(s.value) // unsafe, use for trusted input only
17071707
case _ => error("expected Json.String")
17081708
}
17091709

@@ -1719,7 +1719,7 @@ object JsonCodecDeriverSpec extends SchemaBaseSpec {
17191719
def encodeValue(x: BigDecimal, out: JsonWriter): Unit = out.writeValAsString(x)
17201720

17211721
override def decodeValue(json: Json): BigDecimal = json match {
1722-
case s: Json.String => BigDecimal(s.value)
1722+
case s: Json.String => BigDecimal(s.value) // unsafe, use for trusted input only
17231723
case _ => error("expected Json.String")
17241724
}
17251725

schema/shared/src/test/scala/zio/blocks/schema/json/JsonCodecToJsonSchemaSpec.scala

Lines changed: 12 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -182,7 +182,7 @@ object JsonCodecToJsonSchemaSpec extends SchemaBaseSpec {
182182
val json = jsonSchema.toJson
183183
assertTrue(
184184
json.get("type").one == Right(Json.String("object")),
185-
json.get("additionalProperties").one == Right(Json.Boolean(false))
185+
json.get("maxProperties").one == Right(Json.Number(0))
186186
)
187187
}
188188
),
@@ -292,7 +292,9 @@ object JsonCodecToJsonSchemaSpec extends SchemaBaseSpec {
292292
json.get("properties").get("address").get("properties").get("street").get("type").one == Right(
293293
Json.String("string")
294294
),
295-
json.get("properties").get("employees").get("type").one == Right(Json.String("array"))
295+
json.get("properties").get("employees").get("type").one == Right(
296+
Json.Array(Json.String("array"), Json.String("null"))
297+
)
296298
)
297299
},
298300
test("optional fields are not in required array") {
@@ -325,31 +327,31 @@ object JsonCodecToJsonSchemaSpec extends SchemaBaseSpec {
325327
val jsonSchema = Schema[List[Int]].toJsonSchema
326328
val json = jsonSchema.toJson
327329
assertTrue(
328-
json.get("type").one == Right(Json.String("array")),
330+
json.get("type").one == Right(Json.Array(Json.String("array"), Json.String("null"))),
329331
json.get("items").get("type").one == Right(Json.String("integer"))
330332
)
331333
},
332334
test("Set[String] produces array schema with string items") {
333335
val jsonSchema = Schema[Set[String]].toJsonSchema
334336
val json = jsonSchema.toJson
335337
assertTrue(
336-
json.get("type").one == Right(Json.String("array")),
338+
json.get("type").one == Right(Json.Array(Json.String("array"), Json.String("null"))),
337339
json.get("items").get("type").one == Right(Json.String("string"))
338340
)
339341
},
340342
test("Vector[Double] produces array schema with number items") {
341343
val jsonSchema = Schema[Vector[Double]].toJsonSchema
342344
val json = jsonSchema.toJson
343345
assertTrue(
344-
json.get("type").one == Right(Json.String("array")),
346+
json.get("type").one == Right(Json.Array(Json.String("array"), Json.String("null"))),
345347
json.get("items").get("type").one == Right(Json.String("number"))
346348
)
347349
},
348350
test("List[Person] produces array schema with object items") {
349351
val jsonSchema = Schema[List[Person]].toJsonSchema
350352
val json = jsonSchema.toJson
351353
assertTrue(
352-
json.get("type").one == Right(Json.String("array")),
354+
json.get("type").one == Right(Json.Array(Json.String("array"), Json.String("null"))),
353355
json.get("items").get("type").one == Right(Json.String("object")),
354356
json.get("items").get("properties").get("name").get("type").one == Right(Json.String("string"))
355357
)
@@ -358,8 +360,8 @@ object JsonCodecToJsonSchemaSpec extends SchemaBaseSpec {
358360
val jsonSchema = Schema[List[List[Int]]].toJsonSchema
359361
val json = jsonSchema.toJson
360362
assertTrue(
361-
json.get("type").one == Right(Json.String("array")),
362-
json.get("items").get("type").one == Right(Json.String("array")),
363+
json.get("type").one == Right(Json.Array(Json.String("array"), Json.String("null"))),
364+
json.get("items").get("type").one == Right(Json.Array(Json.String("array"), Json.String("null"))),
363365
json.get("items").get("items").get("type").one == Right(Json.String("integer"))
364366
)
365367
}
@@ -369,15 +371,15 @@ object JsonCodecToJsonSchemaSpec extends SchemaBaseSpec {
369371
val jsonSchema = Schema[Map[String, Int]].toJsonSchema
370372
val json = jsonSchema.toJson
371373
assertTrue(
372-
json.get("type").one == Right(Json.String("object")),
374+
json.get("type").one == Right(Json.Array(Json.String("object"), Json.String("null"))),
373375
json.get("additionalProperties").get("type").one == Right(Json.String("integer"))
374376
)
375377
},
376378
test("Map[String, Person] produces object schema with object additionalProperties") {
377379
val jsonSchema = Schema[Map[String, Person]].toJsonSchema
378380
val json = jsonSchema.toJson
379381
assertTrue(
380-
json.get("type").one == Right(Json.String("object")),
382+
json.get("type").one == Right(Json.Array(Json.String("object"), Json.String("null"))),
381383
json.get("additionalProperties").get("type").one == Right(Json.String("object")),
382384
json.get("additionalProperties").get("properties").get("name").get("type").one == Right(Json.String("string"))
383385
)

schema/shared/src/test/scala/zio/blocks/schema/json/JsonTestUtils.scala

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -74,9 +74,9 @@ object JsonTestUtils {
7474
assert(codec.decode(toHeapByteBuffer(encodedBySchema1), readerConfig))(isRight(equalTo(value))) &&
7575
assert(codec.decode(toDirectByteBuffer(encodedBySchema1), readerConfig))(isRight(equalTo(value))) &&
7676
assert(codec.decode(new String(encodedBySchema1, UTF_8), readerConfig))(isRight(equalTo(value))) && {
77-
val jsonResult = Json.jsonCodec.decode(encodedBySchema6, readerConfig)
78-
assert(jsonResult.flatMap(codec.decode))(isRight(equalTo(value))) /*&&
79-
assert(jsonResult.map(codec.toJsonSchema.check))(isRight(isNone))*/
77+
val result = Json.jsonCodec.decode(encodedBySchema6, readerConfig)
78+
assert(result.flatMap(codec.decode))(isRight(equalTo(value))) /*&&
79+
assert(result.map(codec.toJsonSchema.check))(isRight(isNone))*/
8080
}
8181
}
8282

@@ -98,9 +98,9 @@ object JsonTestUtils {
9898
assert(codec.decode(toHeapByteBuffer(jsonBytes), readerConfig))(isRight(equalTo(expectedValue))) &&
9999
assert(codec.decode(toDirectByteBuffer(jsonBytes), readerConfig))(isRight(equalTo(expectedValue))) &&
100100
assert(codec.decode(json, readerConfig))(isRight(equalTo(expectedValue))) && {
101-
val jsonResult = Json.jsonCodec.decode(json, readerConfig)
102-
assert(jsonResult.flatMap(codec.decode))(isRight(equalTo(expectedValue))) /*&&
103-
assert(jsonResult.map(codec.toJsonSchema.check))(isRight(isNone))*/
101+
val result = Json.jsonCodec.decode(json, readerConfig)
102+
assert(result.flatMap(codec.decode))(isRight(equalTo(expectedValue))) /*&&
103+
assert(result.map(codec.toJsonSchema.check))(isRight(isNone))*/
104104
}
105105
}
106106

@@ -138,9 +138,9 @@ object JsonTestUtils {
138138
if (error.startsWith("malformed byte(s)") || error.startsWith("illegal surrogate")) assertTrue(true)
139139
else assert(codec.decode(new String(invalidJson, UTF_8), readerConfig))(isLeft(hasError(error)))
140140
} && {
141-
val jsonResult = Json.jsonCodec.decode(invalidJson, readerConfig)
142-
assertTrue(jsonResult.flatMap(codec.decode).isLeft) /*&&
143-
assert(jsonResult.map(codec.toJsonSchema.check))(isRight(isSome))*/
141+
val result = Json.jsonCodec.decode(invalidJson, readerConfig)
142+
assertTrue(result.flatMap(codec.decode).isLeft) /*&&
143+
assertTrue(result.map(codec.toJsonSchema.check).fold(_ => true, _.isDefined))*/
144144
}
145145

146146
def encode[A](value: A, expectedJson: String)(implicit schema: Schema[A]): TestResult =

schema/shared/src/test/scala/zio/blocks/schema/json/SchemaToJsonSchemaSpec.scala

Lines changed: 12 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -161,7 +161,7 @@ object SchemaToJsonSchemaSpec extends SchemaBaseSpec {
161161
val json = schema.toJson
162162
assertTrue(
163163
json.get("type").one == Right(Json.String("object")),
164-
json.get("additionalProperties").one == Right(Json.Boolean(false))
164+
json.get("maxProperties").one == Right(Json.Number(0))
165165
)
166166
}
167167
),
@@ -299,7 +299,9 @@ object SchemaToJsonSchemaSpec extends SchemaBaseSpec {
299299
json.get("properties").get("address").get("properties").get("street").get("type").one == Right(
300300
Json.String("string")
301301
),
302-
json.get("properties").get("employees").get("type").one == Right(Json.String("array"))
302+
json.get("properties").get("employees").get("type").one == Right(
303+
Json.Array(Json.String("array"), Json.String("null"))
304+
)
303305
)
304306
},
305307
test("optional fields are not in required array") {
@@ -346,41 +348,35 @@ object SchemaToJsonSchemaSpec extends SchemaBaseSpec {
346348
}
347349
),
348350
suite("Collection types")(
349-
test("List[Int] produces array JSON Schema") {
350-
val schema = Schema[List[Int]].toJsonSchema
351-
val json = schema.toJson
352-
val typeString = json.get("type").one.map(_.asInstanceOf[Json.String].value)
353-
assertTrue(typeString == Right("array"))
354-
},
355351
test("List[Int] produces array schema with integer items") {
356352
val jsonSchema = Schema[List[Int]].toJsonSchema
357353
val json = jsonSchema.toJson
358354
assertTrue(
359-
json.get("type").one == Right(Json.String("array")),
355+
json.get("type").one == Right(Json.Array(Json.String("array"), Json.String("null"))),
360356
json.get("items").get("type").one == Right(Json.String("integer"))
361357
)
362358
},
363359
test("Set[String] produces array schema with string items") {
364360
val jsonSchema = Schema[Set[String]].toJsonSchema
365361
val json = jsonSchema.toJson
366362
assertTrue(
367-
json.get("type").one == Right(Json.String("array")),
363+
json.get("type").one == Right(Json.Array(Json.String("array"), Json.String("null"))),
368364
json.get("items").get("type").one == Right(Json.String("string"))
369365
)
370366
},
371367
test("Vector[Double] produces array schema with number items") {
372368
val jsonSchema = Schema[Vector[Double]].toJsonSchema
373369
val json = jsonSchema.toJson
374370
assertTrue(
375-
json.get("type").one == Right(Json.String("array")),
371+
json.get("type").one == Right(Json.Array(Json.String("array"), Json.String("null"))),
376372
json.get("items").get("type").one == Right(Json.String("number"))
377373
)
378374
},
379375
test("List[Person] produces array schema with object items") {
380376
val jsonSchema = Schema[List[Person]].toJsonSchema
381377
val json = jsonSchema.toJson
382378
assertTrue(
383-
json.get("type").one == Right(Json.String("array")),
379+
json.get("type").one == Right(Json.Array(Json.String("array"), Json.String("null"))),
384380
json.get("items").get("type").one == Right(Json.String("object")),
385381
json.get("items").get("properties").get("name").get("type").one == Right(Json.String("string"))
386382
)
@@ -389,32 +385,26 @@ object SchemaToJsonSchemaSpec extends SchemaBaseSpec {
389385
val jsonSchema = Schema[List[List[Int]]].toJsonSchema
390386
val json = jsonSchema.toJson
391387
assertTrue(
392-
json.get("type").one == Right(Json.String("array")),
393-
json.get("items").get("type").one == Right(Json.String("array")),
388+
json.get("type").one == Right(Json.Array(Json.String("array"), Json.String("null"))),
389+
json.get("items").get("type").one == Right(Json.Array(Json.String("array"), Json.String("null"))),
394390
json.get("items").get("items").get("type").one == Right(Json.String("integer"))
395391
)
396392
}
397393
),
398394
suite("Map types")(
399-
test("Map[String, Int] produces object JSON Schema") {
400-
val schema = Schema[Map[String, Int]].toJsonSchema
401-
val json = schema.toJson
402-
val typeString = json.get("type").one.map(_.asInstanceOf[Json.String].value)
403-
assertTrue(typeString == Right("object"))
404-
},
405395
test("Map[String, Int] produces object schema with additionalProperties") {
406396
val jsonSchema = Schema[Map[String, Int]].toJsonSchema
407397
val json = jsonSchema.toJson
408398
assertTrue(
409-
json.get("type").one == Right(Json.String("object")),
399+
json.get("type").one == Right(Json.Array(Json.String("object"), Json.String("null"))),
410400
json.get("additionalProperties").get("type").one == Right(Json.String("integer"))
411401
)
412402
},
413403
test("Map[String, Person] produces object schema with object additionalProperties") {
414404
val jsonSchema = Schema[Map[String, Person]].toJsonSchema
415405
val json = jsonSchema.toJson
416406
assertTrue(
417-
json.get("type").one == Right(Json.String("object")),
407+
json.get("type").one == Right(Json.Array(Json.String("object"), Json.String("null"))),
418408
json.get("additionalProperties").get("type").one == Right(Json.String("object")),
419409
json.get("additionalProperties").get("properties").get("name").get("type").one == Right(Json.String("string"))
420410
)

0 commit comments

Comments
 (0)