diff --git a/restdocs-api-spec-jsonschema/src/main/kotlin/com/epages/restdocs/apispec/jsonschema/JsonSchemaFromFieldDescriptorsGenerator.kt b/restdocs-api-spec-jsonschema/src/main/kotlin/com/epages/restdocs/apispec/jsonschema/JsonSchemaFromFieldDescriptorsGenerator.kt index 569a612..d9b1115 100644 --- a/restdocs-api-spec-jsonschema/src/main/kotlin/com/epages/restdocs/apispec/jsonschema/JsonSchemaFromFieldDescriptorsGenerator.kt +++ b/restdocs-api-spec-jsonschema/src/main/kotlin/com/epages/restdocs/apispec/jsonschema/JsonSchemaFromFieldDescriptorsGenerator.kt @@ -15,6 +15,7 @@ import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper import org.everit.json.schema.ArraySchema import org.everit.json.schema.BooleanSchema import org.everit.json.schema.CombinedSchema +import org.everit.json.schema.CombinedSchema.oneOf import org.everit.json.schema.EmptySchema import org.everit.json.schema.EnumSchema import org.everit.json.schema.NullSchema @@ -206,9 +207,26 @@ class JsonSchemaFromFieldDescriptorsGenerator { ) : FieldDescriptor(path, description, type, optional, ignored, attributes) { fun jsonSchemaType(): Schema { - val schemaBuilders = jsonSchemaPrimitiveTypes.map { typeToSchema(it) } - return if (schemaBuilders.size == 1) schemaBuilders.first().description(description).build() - else CombinedSchema.oneOf(schemaBuilders.map { it.build() }).description(description).build() + val schemaBuilders: List> + if (jsonSchemaPrimitiveTypes.size > 1 && + optional && + !jsonSchemaPrimitiveTypes.contains("null") + ) { + schemaBuilders = jsonSchemaPrimitiveTypes + .plus(jsonSchemaPrimitiveTypeFromDescriptorType("null")) + .map { typeToSchema(it) } + } else { + schemaBuilders = jsonSchemaPrimitiveTypes.map { typeToSchema(it) } + } + return if (schemaBuilders.size == 1) schemaBuilders.first().description(description).checkNullable().build() + else oneOf(schemaBuilders.map { it.build() }).description(description).checkNullable().build() + } + + private fun Schema.Builder.checkNullable(): Schema.Builder { + if (optional) { + this.nullable(true) + } + return this } fun merge(fieldDescriptor: FieldDescriptor): FieldDescriptorWithSchemaType { @@ -230,7 +248,9 @@ class JsonSchemaFromFieldDescriptorsGenerator { private fun typeToSchema(type: String): Schema.Builder<*> = when (type) { - "null" -> NullSchema.builder() + "null" -> { + NullSchema.builder().nullable() + } "empty" -> EmptySchema.builder() "object" -> ObjectSchema.builder() "array" -> ArraySchema.builder().applyConstraints(this).allItemSchema(arrayItemsSchema()) @@ -246,9 +266,14 @@ class JsonSchemaFromFieldDescriptorsGenerator { else -> throw IllegalArgumentException("unknown field type $type") } + private fun NullSchema.Builder.nullable(): NullSchema.Builder { + this.nullable(true) + return this + } + private fun arrayItemsSchema(): Schema { return attributes.itemsType - ?.let { typeToSchema(it.toLowerCase()).build() } + ?.let { typeToSchema(it.lowercase()).build() } ?: CombinedSchema.oneOf( listOf( ObjectSchema.builder().build(), @@ -277,7 +302,7 @@ class JsonSchemaFromFieldDescriptorsGenerator { ) private fun jsonSchemaPrimitiveTypeFromDescriptorType(fieldDescriptorType: String) = - fieldDescriptorType.toLowerCase() + fieldDescriptorType.lowercase() .let { if (it == "varies") "empty" else it } // varies is used by spring rest docs if the type is ambiguous - in json schema we want to represent as empty } } diff --git a/restdocs-api-spec-jsonschema/src/test/kotlin/com/epages/restdocs/apispec/jsonschema/JsonSchemaFromFieldDescriptorsGeneratorTest.kt b/restdocs-api-spec-jsonschema/src/test/kotlin/com/epages/restdocs/apispec/jsonschema/JsonSchemaFromFieldDescriptorsGeneratorTest.kt index d729922..dddf15e 100644 --- a/restdocs-api-spec-jsonschema/src/test/kotlin/com/epages/restdocs/apispec/jsonschema/JsonSchemaFromFieldDescriptorsGeneratorTest.kt +++ b/restdocs-api-spec-jsonschema/src/test/kotlin/com/epages/restdocs/apispec/jsonschema/JsonSchemaFromFieldDescriptorsGeneratorTest.kt @@ -19,6 +19,7 @@ import org.everit.json.schema.Schema import org.everit.json.schema.StringSchema import org.everit.json.schema.ValidationException import org.everit.json.schema.loader.SchemaLoader +import org.everit.json.schema.loader.internal.DefaultSchemaClient import org.json.JSONArray import org.json.JSONObject import org.junit.jupiter.api.Test @@ -61,6 +62,7 @@ class JsonSchemaFromFieldDescriptorsGeneratorTest { val shippingAddressSchema = objectSchema.propertySchemas["shippingAddress"]!! then(shippingAddressSchema).isInstanceOf(ObjectSchema::class.java) then(shippingAddressSchema.description).isNotEmpty() + then(shippingAddressSchema.isNullable).isTrue() then(objectSchema.definesProperty("billingAddress")).isTrue() val billingAddressSchema = objectSchema.propertySchemas["billingAddress"] as ObjectSchema @@ -120,6 +122,7 @@ class JsonSchemaFromFieldDescriptorsGeneratorTest { then(pagePositiveSchema.minimum.toInt()).isEqualTo(1) then(pagePositiveSchema.maximum).isNull() then(pagePositiveSchema.requiresInteger()).isTrue + then(pagePositiveSchema.isNullable).isTrue() then(objectSchema.definesProperty("page100_200")).isTrue then(objectSchema.propertySchemas["page100_200"]).isInstanceOf(NumberSchema::class.java) @@ -445,7 +448,14 @@ class JsonSchemaFromFieldDescriptorsGeneratorTest { private fun whenSchemaGenerated() { schemaString = generator.generateSchema(fieldDescriptors!!) println(schemaString) - schema = SchemaLoader.load(JSONObject(schemaString)) + schema = SchemaLoader + .builder() + .nullableSupport(true) + .schemaJson(JSONObject(schemaString)) + .schemaClient(DefaultSchemaClient()) + .build() + .load() + .build() } private fun givenFieldDescriptorWithPrimitiveArray() { @@ -589,9 +599,9 @@ class JsonSchemaFromFieldDescriptorsGeneratorTest { private fun givenDifferentFieldDescriptorsWithSamePathAndDifferentTypes() { fieldDescriptors = listOf( - FieldDescriptor("id", "some", "STRING"), - FieldDescriptor("id", "some", "NULL"), - FieldDescriptor("id", "some", "BOOLEAN") + FieldDescriptor("id", "some", "STRING", true), + FieldDescriptor("id", "some", "NULL", true), + FieldDescriptor("id", "some", "BOOLEAN", true) ) }