Why isn't Swagger detecting optional JSON properties? - kotlin

I have the following class:
import com.fasterxml.jackson.annotation.JsonProperty
import org.joda.time.DateTime
import org.joda.time.DateTimeZone
data class Entity(
val email: String,
val name: String,
val birthDate: DateTime,
#JsonProperty(required = false) val gender: Gender? = null,
#JsonProperty(required = false) val country: String? = null,
val locale: String,
val disabled: Boolean = false,
#JsonProperty(required = false) val createdAt: DateTime = DateTime(DateTimeZone.UTC),
val role: Role,
val entityTypeId: Long,
val entityTypeAttributes: MutableMap<String, Any> = HashMap(),
val medicalSpecialityId: Long? = null,
val id: Long? = null
)
And some properties are not required, because they are either nullable (gender, country), or they have a default value (createdAt).
However, the generated swagger documentation is as follows:
"components": {
"schemas": {
"Entity": {
"required": [
"birthDate",
"createdAt", <------------ Notice here!
"disabled",
"email",
"entityTypeAttributes",
"entityTypeId",
"locale",
"name",
"role"
],
"type": "object",
"properties": {
"email": {
"type": "string"
},
"name": {
"type": "string"
},
"birthDate": {
"type": "string",
"format": "date-time"
},
"gender": {
"type": "string",
"enum": [
"MALE",
"FEMALE",
"OTHER"
]
},
"country": {
"type": "string"
},
"locale": {
"type": "string"
},
"disabled": {
"type": "boolean"
},
"createdAt": {
"type": "string",
"format": "date-time"
},
"role": {
"type": "string",
"enum": [
"ADMIN",
"DOCTOR",
"PATIENT"
]
},
"entityTypeId": {
"type": "integer",
"format": "int64"
},
"entityTypeAttributes": {
"type": "object",
"additionalProperties": {
"type": "object"
}
},
"medicalSpecialityId": {
"type": "integer",
"format": "int64"
},
"id": {
"type": "integer",
"format": "int64"
}
}
},
(...)
So in terms of documentation it shows that createdAt is mandatory (which is not true)...
Generated Swagger docs
I am using Kotlin, Javalin and the OpenAPI (io.javalin.plugin.openapi) Javalin integration.
I don't know what more do I need to make OpenAPI understand that createdAt is optional...

My guess would be that the kotlin implementation is using nullability as a way to discover required and ignoring the required property. For example, you shouldn't actually need the annotation for gender and country.
Obviously this is not ideal, but if you change creadtedAt to a DateTime? it would not show as required.
This is likely a bug with the kotlin openapi doc tool that javalin has pulled in.

Related

Generated OpenAPI golang client doesn't seem to handle dates correctly?

I'm working on an API that provides a list of transactions. I'm writing it in Java/Kotlin Spring, but prefer golang for CLIs, so I'm generating a golang client for it. The API works well in the Swagger UI.
Kotlin API:
#GetMapping
fun listTransactions() : ResponseEntity<List<Transaction>> {
val transactions = ArrayList<Transaction>()
transactionRepository.findAll().mapTo(transactions) { fromEntity(it) }
return ResponseEntity.ok(transactions)
}
Kotlin Object:
data class Transaction(
val id: Long,
val transactionDate: Date, // Java SQL date
val postedDate: Date?, // Java SQL date
val amount: BigDecimal,
val category: Category,
val merchant: Merchant,
val merchantDescription: String?
)
Generated Schema:
{
"openapi": "3.0.1",
"info": {
"title": "BFI Swagger Title",
"description": "BFI description",
"version": "0.1"
},
"servers": [{
"url": "http://localhost:8080",
"description": "Generated server url"
}],
"paths": {
"/transaction": {
"get": {
"tags": ["transaction-router"],
"operationId": "listTransactions",
"responses": {
"200": {
"description": "OK",
"content": {
"*/*": {
"schema": {
"type": "array",
"items": {
"$ref": "#/components/schemas/Transaction"
}
}
}
}
}
}
},
"post": {
"tags": ["transaction-router"],
"operationId": "createTransaction",
"requestBody": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/CreateTransactionRequest"
}
}
},
"required": true
},
"responses": {
"200": {
"description": "OK",
"content": {
"*/*": {
"schema": {
"$ref": "#/components/schemas/Transaction"
}
}
}
}
}
}
},
"/hello": {
"get": {
"tags": ["category-router"],
"summary": "Hello there!",
"operationId": "hello",
"responses": {
"200": {
"description": "OK",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Category"
}
}
}
}
}
}
}
},
"components": {
"schemas": {
"CreateTransactionRequest": {
"type": "object",
"properties": {
"transactionDate": {
"type": "string",
"format": "date-time"
},
"postedDate": {
"type": "string",
"format": "date-time"
},
"amount": {
"type": "number"
},
"categoryId": {
"type": "integer",
"format": "int64"
},
"merchantId": {
"type": "integer",
"format": "int64"
},
"merchantDescription": {
"type": "string"
}
}
},
"Category": {
"type": "object",
"properties": {
"id": {
"type": "integer",
"format": "int64"
},
"name": {
"type": "string"
},
"note": {
"type": "string"
}
}
},
"Merchant": {
"type": "object",
"properties": {
"id": {
"type": "integer",
"format": "int64"
},
"name": {
"type": "string"
},
"note": {
"type": "string"
}
}
},
"Transaction": {
"type": "object",
"properties": {
"id": {
"type": "integer",
"format": "int64"
},
"transactionDate": {
"type": "string",
"format": "date-time"
},
"postedDate": {
"type": "string",
"format": "date-time"
},
"amount": {
"type": "number"
},
"category": {
"$ref": "#/components/schemas/Category"
},
"merchant": {
"$ref": "#/components/schemas/Merchant"
},
"merchantDescription": {
"type": "string"
}
}
}
}
}
}
Golang Client Object:
type Transaction struct {
Id *int64 `json:"id,omitempty"`
TransactionDate *time.Time `json:"transactionDate,omitempty"`
PostedDate *time.Time `json:"postedDate,omitempty"`
Amount *float32 `json:"amount,omitempty"`
Category *Category `json:"category,omitempty"`
Merchant *Merchant `json:"merchant,omitempty"`
MerchantDescription *string `json:"merchantDescription,omitempty"`
}
All of this seems correct enough. However, when I use the OpenAPI client, it seems like deserialization isn't working correctly:
Error when calling `TransactionRouterApi.ListTransactions``: parsing time "\"2022-10-28\"" as "\"2006-01-02T15:04:05Z07:00\"": cannot parse "\"" as "T"
Full HTTP response: &{200 200 HTTP/1.1 1 1 map[Content-Type:[application/json] Date:[Sat, 29 Oct 2022 04:03:31 GMT]] {[{"id":1,"transactionDate":"2022-10-28","postedDate":"2022-10-28","amount":0.00,"category":{"id":1,"name":"test","note":"test"},"merchant":{"id":1,"name":"test","note":"test"},"merchantDescription":null},{"id":2,"transactionDate":"2022-10-28","postedDate":"2022-10-28","amount":0.00,"category":{"id":1,"name":"test","note":"test"},"merchant":{"id":1,"name":"test","note":"test"},"merchantDescription":null},{"id":3,"transactionDate":"2022-10-28","postedDate":"2022-10-28","amount":0.00,"category":{"id":1,"name":"test","note":"test"},"merchant":{"id":1,"name":"test","note":"test"},"merchantDescription":null}]} -1 [chunked] false false map[] 0x140001daf00 <nil>}
Response from `TransactionRouterApi.ListTransactions`: [{0x140000aa218 0001-01-01 00:00:00 +0000 UTC <nil> <nil> <nil> <nil> <nil>}]
Am I doing something incorrectly that results in deserialization failing? Or is this a client bug (seems doubtful, but who knows).
I've looked at the generation arguments I used and the schema available at my endpoint, and they both appear correct.
Script executed: openapi-generator-cli generate -g go -i http://localhost:8080/v3/api-docs
Two options:
Create a custom type and implement the Unmarshaler interface for the date fields.
Return valid RFC 3339 date/times from the API.

Why oneOf does not work on my schema in jsonschema?

My login has different payloads one is:
{
"username": "",
"pass": ""
}
And one of the other is:
{
"username": "",
"pass": "",
"facebook": true
}
And the last:
{
"username": "",
"pass": "",
"google": true
}
My schema is as follow:
login_schema = {
"title": "UserLogin",
"description": "User login with facebook, google or regular login.",
"type": "object",
"properties": {
"username": {
"type": "string"
},
"pass": {
"type": "string"
},
"facebook": {
"type": "string"
},
"google": {
"type": "string"
}
},
"oneOf": [
{
"required": [
"username",
"pass"
],
"additionalProperties": False,
},
{
"required": [
"username",
"pass"
"google"
]
},
{
"required": [
"username",
"pass",
"facebook"
]
}
],
"minProperties": 2,
"additionalProperties": False,
}
It should give an error for the below sample:
{
"username": "",
"pass": "",
"google": "",
"facebook": ""
}
But it validates the schema successfully! What I have done wrong in the above schema?
EDIT-1:
pip3 show jsonschema
Name: jsonschema
Version: 3.0.2
Summary: An implementation of JSON Schema validation for Python
Home-page: https://github.com/Julian/jsonschema
Author: Julian Berman
Author-email: Julian#GrayVines.com
License: UNKNOWN
Location: /usr/local/lib/python3.7/site-packages
Requires: setuptools, six, attrs, pyrsistent
EDIT-2:
What I get as an error is:
jsonschema.exceptions.ValidationError: {'username': '', 'pass': '', 'google': '12'} is valid under each of {'required': ['username', 'pass', 'google']}, {'required': ['username', 'pass']}
A live demo of the error: https://jsonschema.dev/s/mXg5X
Your solution is really close. You just need to change /oneOf/0 to
{
"properties": {
"username": true,
"pass": true
},
"required": ["username", "pass"],
"additionalProperties": false
}
The problem is that additionalProperties doesn't consider the required keyword when determining what properties are considered "additional". It considers only properties and patternProperties. When just using required, additionalProperties considers all properties to be "additional" and the only valid value is {}.
However, I suggest a different approach. The dependencies keyword is useful in these situations.
{
"type": "object",
"properties": {
"username": { "type": "string" },
"pass": { "type": "string" },
"facebook": { "type": "boolean" },
"google": { "type": "boolean" }
},
"required": ["username", "pass"],
"dependencies": {
"facebook": { "not": { "required": ["google"] } },
"google": { "not": { "required": ["facebook"] } }
},
"additionalProperties": false
}

json schema - field is required based on another field value

I'm working on creating a JsonSchema(v4).
I'm trying to make one property required based off the value of another property from it's parent.
Parent
User
subtype
address
Child
Address
line1
line2
companyName (required if user subtype is company)
How could this be done?
I have something like this now...
{
"User": {
"title": "User",
"type": "object",
"id": "#User",
"properties": {
"subtype": {
"type": "string"
},
"address": {
"$ref": "Address"
}
}
}
"Address": {
"title": "Address",
"type": "object",
"id": "#Address",
"properties": {
"line1": {
"type": "string"
},
"line2": {
"type": "string"
},
"companyName": {
"type": "string"
}
},
"required": ["line1", "line2"]
}
}
Subtype is a arbitrary string, so a full list of the different subtypes is not possible.
Add this to your User schema. Essentially it reads as: either "subtype" is not "company" or "address" requires "companyName".
"anyOf": [
{
"not": {
"properties": {
"subtype": { "enum": ["company"] }
}
}
},
{
"properties": {
"address": {
"required": ["companyName"]
}
}
}
]

JSON SCHEMA - How can i check a values exists in an array

I have a simple question. I have a property groupBy which is an array and contain only two possible values "product" and "date". Now i want to make another property required based upon a value exists in the groupBy array. In this case when my groupBy array contains "date" i want to make resolution required! How can i do that ?
Who can i check if an array contains a value ?
var data = {
"pcsStreamId": 123123,
"start": moment().valueOf(),
"end": moment().valueOf(),
"groupBy" : ["product"]
};
var schema = {
"type": "object",
"properties": {
"pcsStreamId": { "type": "number" },
"start": { "type": "integer", "minimum" : 0 },
"end": { "type": "integer", "minimum" : 0 },
"groupBy": {
"type": "array",
"uniqueItems": true,
"items" : {
"type": "string",
"enum": ["product", "date"]
},
"oneOf": [
{
"contains": { "enum": ["date"] },
"required": ["resolution"]
}
]
},
"resolution" : {
"type": "string",
"enum": ["day", "year", "month", "shift"]
},
},
"required": ["pcsStreamId", "start", "end", "groupBy"]
};
To solve the problem we have to use a boolean logic concept called implication. To put the requirement in boolean logic terms, we would say "groupBy" contains "date" implies that "resolution" is required. Implication can be expressed as "(not A) or B". In other words, either "groupBy" does not contain "date", or "resolution" is required. In this form, it should be more clear how to implement the solution.
{
"type": "object",
"properties": {
"pcsStreamId": { "type": "number" },
"start": { "type": "integer", "minimum": 0 },
"end": { "type": "integer", "minimum": 0 },
"groupBy": {
"type": "array",
"uniqueItems": true,
"items": { "enum": ["product", "date"] }
},
"resolution": { "enum": ["day", "year", "month", "shift"] }
},
"required": ["pcsStreamId", "start", "end", "groupBy"],
"anyOf": [
{ "not": { "$ref": "#/definitions/contains-date" } },
{ "required": ["resolution"] }
],
"definitions": {
"contains-date": {
"properties": {
"groupBy": {
"contains": { "enum": ["date"] }
}
}
}
}
}
Edit
This answer uses the new draft-06 contains keyword. I used it because the questioner used it, but if you are on draft-04, you can use this definition of "contains-date" instead. It uses another logic identity (∃x A <=> ¬∀x ¬A) to get the functionality of the contains keyword.
{
"definitions": {
"contains-date": {
"properties": {
"groupBy": {
"not": {
"items": {
"not": { "enum": ["date"] }
}
}
}
}
}
}
}
Your groupby property seems to be an object. May be its the nested enum property that you are talking about ( since that is an array and contains date and product). However, once you parsed this json to an object you can check something like:
groupby.items.enums.indexOf('date') === -1
array.indexOf(anyItem) returns the index of the item, if present in the array else it returns -1.
Hope that helps

JSON Schema for collections

I am looking for how to write a JSON schema for collection of objects within an object.
{
"name": "Sadiq",
"age": 68,
"email": [
{
"emailid": "sadiq#gmail.com"
},
{
"emailid": "sadiq#yahoo.com"
}
],
"phone": [
{
"phonenumber": "301-215-8006"
},
{
"phonenumber": "301-215-8007"
}
]
}
Here is one possible way to write this schema:
{
"type": "object",
"additionalProperties": false,
"properties": {
"name": {
"type": "string",
"required": true
},
"age": {
"type": "integer",
"required": true
},
"email": {
"type": "array",
"items": {
"type": "object",
"additionalProperties": false,
"properties": {
"emailid": {
"type": "string",
"required": true
}
}
}
},
"phone": {
"type": "array",
"items": {
"type": "object",
"additionalProperties": false,
"properties": {
"phonenumber": {
"type": "string",
"required": true
}
}
}
}
}
}
Possible improvements would be:
Add a regex pattern to strongly validate the emailid field
Extract email and phone into top level types and refer to them in the above schema.
You can have a try of csonschema, which let you write jsonschema with a more easier way.
name: 'string'
age: 'integer'
email: ['email']
phone: ['string']
In python, there is json library which can help you encode or reformat as u need