Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -3287,7 +3287,7 @@ protected void resolveSchemaMembers(Schema schema, Annotated a, Annotation[] ann
schema.setContentMediaType(contentMediaType);
}
if (schemaAnnotation.examples().length > 0) {
List<Object> parsedExamples = io.swagger.v3.core.util.AnnotationsUtils.parseExamplesArray(schemaAnnotation);
List<Object> parsedExamples = io.swagger.v3.core.util.AnnotationsUtils.parseExamplesArray(schemaAnnotation, schema);
if (schema.getExamples() == null || schema.getExamples().isEmpty()) {
schema.setExamples(parsedExamples);
} else {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,7 @@ public static boolean hasSchemaAnnotation(io.swagger.v3.oas.annotations.media.Sc
&& StringUtils.isBlank(schema._const())
&& schema.additionalProperties().equals(io.swagger.v3.oas.annotations.media.Schema.AdditionalPropertiesValue.USE_ADDITIONAL_PROPERTIES_ANNOTATION)
&& schema.additionalPropertiesSchema().equals(Void.class)
&& schema.examples().length == 0
) {
return false;
}
Expand Down Expand Up @@ -586,7 +587,7 @@ private static void applyArraySchemaAnnotation(io.swagger.v3.oas.annotations.med
arraySchemaObject.setWriteOnly(null);
}
if (openapi31 && arraySchemaAnnotation.examples().length > 0) {
arraySchemaObject.setExamples(parseExamplesArray(arraySchemaAnnotation));
arraySchemaObject.setExamples(parseExamplesArray(arraySchemaAnnotation, arraySchemaObject));
}
}

Expand Down Expand Up @@ -777,7 +778,7 @@ public static Optional<Schema> getSchemaFromAnnotation(
schemaObject.setUnevaluatedProperties(resolveSchemaFromType(schema.unevaluatedProperties(), components, jsonViewAnnotation, openapi31, null, null, context));
}
if (openapi31 && schema.examples().length > 0) {
schemaObject.setExamples(parseExamplesArray(schema));
schemaObject.setExamples(parseExamplesArray(schema, schemaObject));
}

if (schema.defaultValue() != null && !DEFAULT_SENTINEL.equals(schema.defaultValue())) {
Expand Down Expand Up @@ -917,7 +918,7 @@ private static void setExampleSchema(io.swagger.v3.oas.annotations.media.Schema
// Only parse "null" as null value when nullable=true
if (node.isNull() && schema.nullable()) {
schemaObject.setExample(null);
} else if (node.isObject() || node.isArray()) {
} else if (shouldUseNodeAsExample(node, schemaObject)) {
schemaObject.setExample(node);
} else {
schemaObject.setExample(exampleValue);
Expand All @@ -927,6 +928,19 @@ private static void setExampleSchema(io.swagger.v3.oas.annotations.media.Schema
}
}

private static boolean shouldUseNodeAsExample(JsonNode node, Schema schemaObject) {
if (node.isObject() || node.isArray()) {
return true;
}
if (schemaObject != null && SchemaTypeUtils.isNumberSchema(schemaObject)) {
return true;
}
if (schemaObject != null && SchemaTypeUtils.isStringSchema(schemaObject)) {
return false;
}
return node.isNumber();
}

private static void setDefaultSchema(io.swagger.v3.oas.annotations.media.Schema schema, boolean openapi31, Schema schemaObject) {
String defaultValue = schema.defaultValue().trim();
final ObjectMapper mapper = openapi31 ? Json31.mapper() : Json.mapper();
Expand All @@ -947,6 +961,10 @@ private static void setDefaultSchema(io.swagger.v3.oas.annotations.media.Schema
}

public static List<Object> parseExamplesArray(io.swagger.v3.oas.annotations.media.Schema schema) {
return parseExamplesArray(schema, null);
}

public static List<Object> parseExamplesArray(io.swagger.v3.oas.annotations.media.Schema schema, Schema schemaObject) {
String[] examplesArray = schema.examples();
List<Object> parsedExamples = new ArrayList<>();
final ObjectMapper mapper = Json31.mapper();
Expand All @@ -963,7 +981,9 @@ public static List<Object> parseExamplesArray(io.swagger.v3.oas.annotations.medi
// Only parse "null" as null value when nullable=true
if (node.isNull() && schema.nullable()) {
parsedExamples.add(null);
} else if (node.isObject() || node.isArray()) {
} else if (schemaObject == null && "string".equals(schema.type())) {
parsedExamples.add(trimmed);
} else if (shouldUseNodeAsExample(node, schemaObject)) {
parsedExamples.add(node);
} else {
parsedExamples.add(trimmed);
Expand All @@ -983,7 +1003,12 @@ public static Schema resolveSchemaFromType(Class<?> schemaImplementation, Compon
public static Schema resolveSchemaFromType(Class<?> schemaImplementation, Components components, JsonView jsonViewAnnotation, boolean openapi31) {
return resolveSchemaFromType(schemaImplementation, components, jsonViewAnnotation, openapi31, null, null, null);
}
public static Schema resolveSchemaFromType(Class<?> schemaImplementation, Components components, JsonView jsonViewAnnotation, boolean openapi31, io.swagger.v3.oas.annotations.media.Schema schemaAnnotation, io.swagger.v3.oas.annotations.media.ArraySchema arrayAnnotation, ModelConverterContext context) {
public static Schema resolveSchemaFromType(Class<?> schemaImplementation,
Components components,
JsonView jsonViewAnnotation,
boolean openapi31,
io.swagger.v3.oas.annotations.media.Schema schemaAnnotation,
io.swagger.v3.oas.annotations.media.ArraySchema arrayAnnotation, ModelConverterContext context) {
Schema schemaObject;
PrimitiveType primitiveType = PrimitiveType.fromType(schemaImplementation);
if (primitiveType != null) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,185 @@
package io.swagger.v3.core.converting;

import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import io.swagger.v3.core.converter.ModelConverters;
import io.swagger.v3.core.converter.ResolvedSchema;
import io.swagger.v3.core.util.Json;
import io.swagger.v3.core.util.Json31;
import org.testng.annotations.Test;

import java.math.BigDecimal;

import static org.testng.Assert.assertNotNull;
import static org.testng.Assert.assertTrue;

/**
* test documenting how example values for numbers/non-number differ in their JSON-representation.
*/
public class Issue5061Test {


@Test
public void testExampleValuesAreSerializedAsJsonDifferentlyBetweenStringAndNumber() throws Exception {
ResolvedSchema schema = ModelConverters.getInstance(true).readAllAsResolvedSchema(
ModelWithDifferentCombinationOfNumberFieldsWithExamples.class
);

assertNotNull(schema, "Schema should resolve");
String json = Json31.pretty(schema);
assertNotNull(json);
ObjectMapper mapper = new ObjectMapper();
JsonNode root = mapper.readTree(json);

JsonNode stringFieldType = root.at("/schema/properties/stringFieldType");
assertExampleIsString(stringFieldType);

JsonNode stringFieldTypeWithExplicitStringSchemaType = root.at("/schema/properties/stringFieldTypeWithExplicitStringSchemaType");
assertExampleIsString(stringFieldTypeWithExplicitStringSchemaType);

JsonNode stringFieldTypeWithExplicitNumberSchemaType = root.at("/schema/properties/stringFieldTypeWithExplicitNumberSchemaType");
assertExampleIsNumber(stringFieldTypeWithExplicitNumberSchemaType);

JsonNode stringFieldTypeWithExplicitIntegerSchemaType = root.at("/schema/properties/stringFieldTypeWithExplicitIntegerSchemaType");
assertExampleIsNumber(stringFieldTypeWithExplicitIntegerSchemaType);

JsonNode bigDecimalFieldTypeWithExplicitStringSchemaType = root.at("/schema/properties/bigDecimalFieldTypeWithExplicitStringSchemaType");
assertExampleIsString(bigDecimalFieldTypeWithExplicitStringSchemaType);

JsonNode bigDecimalFieldType = root.at("/schema/properties/bigDecimalFieldType");
assertExampleIsNumber(bigDecimalFieldType);
}

@Test
public void testDefaultValueNumericConversionThroughModelResolver() throws Exception {
ResolvedSchema schema = ModelConverters.getInstance(false).readAllAsResolvedSchema(
ModelWithDefaultValues.class
);

assertNotNull(schema, "Schema should resolve");
String json = Json.pretty(schema);
assertNotNull(json);
ObjectMapper mapper = new ObjectMapper();
JsonNode root = mapper.readTree(json);

JsonNode bigDecimalDefault = root.at("/schema/properties/bigDecimalWithDefault");
assertTrue(bigDecimalDefault.get("default").isNumber(),
"default on BigDecimal field should be serialized as a JSON number");

JsonNode stringDefault = root.at("/schema/properties/stringWithDefault");
assertTrue(stringDefault.get("default").isTextual(),
"default on explicit type=\"string\" field should remain a JSON string");
}

@Test
public void testExamplesArrayValuesAreSerializedAsJsonNumbers() throws Exception {
ResolvedSchema schema = ModelConverters.getInstance(true).readAllAsResolvedSchema(
ModelWithExamplesArray.class
);

assertNotNull(schema, "Schema should resolve");
String json = Json31.pretty(schema);
assertNotNull(json);
ObjectMapper mapper = new ObjectMapper();
JsonNode root = mapper.readTree(json);

JsonNode bigDecimalExamples = root.at("/schema/properties/bigDecimalWithExamples");
assertNotNull(bigDecimalExamples.get("examples"), "examples key should exist in JSON, full node: " + bigDecimalExamples);
assertTrue(bigDecimalExamples.get("examples").get(0).isNumber(),
"examples on BigDecimal field should be serialized as JSON numbers");
assertTrue(bigDecimalExamples.get("examples").get(1).isNumber(),
"examples on BigDecimal field should be serialized as JSON numbers");

JsonNode stringExamples = root.at("/schema/properties/stringWithExamples");
assertTrue(stringExamples.get("examples").get(0).isTextual(),
"examples on explicit type=\"string\" field should remain JSON strings");
}

private void assertExampleIsNumber(JsonNode node) {
assertTrue(node.get("example").isNumber(), "should be a number");
}

private void assertExampleIsString(JsonNode node) {
assertTrue(node.get("example").isTextual(), "should be a string");
}

public static class ModelWithDefaultValues {

@io.swagger.v3.oas.annotations.media.Schema(defaultValue = "10.00")
BigDecimal bigDecimalWithDefault;

@io.swagger.v3.oas.annotations.media.Schema(defaultValue = "42", type = "string")
String stringWithDefault;

public BigDecimal getBigDecimalWithDefault() {
return bigDecimalWithDefault;
}

public String getStringWithDefault() {
return stringWithDefault;
}
}

public static class ModelWithExamplesArray {

@io.swagger.v3.oas.annotations.media.Schema(examples = {"10.00", "20.50"})
BigDecimal bigDecimalWithExamples;

@io.swagger.v3.oas.annotations.media.Schema(examples = {"hello", "world"}, type = "string")
String stringWithExamples;

public BigDecimal getBigDecimalWithExamples() {
return bigDecimalWithExamples;
}

public String getStringWithExamples() {
return stringWithExamples;
}
}

public static class ModelWithDifferentCombinationOfNumberFieldsWithExamples {

@io.swagger.v3.oas.annotations.media.Schema(example = "5 lacs per annum")
String stringFieldType;

@io.swagger.v3.oas.annotations.media.Schema(type = "string", example = "5 lacs per annum")
String stringFieldTypeWithExplicitStringSchemaType;

@io.swagger.v3.oas.annotations.media.Schema(type = "number", example = "10")
String stringFieldTypeWithExplicitNumberSchemaType;

@io.swagger.v3.oas.annotations.media.Schema(type = "integer", example = "5")
String stringFieldTypeWithExplicitIntegerSchemaType;

@io.swagger.v3.oas.annotations.media.Schema(type = "string", example = "13.37")
BigDecimal bigDecimalFieldTypeWithExplicitStringSchemaType;

@io.swagger.v3.oas.annotations.media.Schema(example = "13.37")
BigDecimal bigDecimalFieldType;

public String getStringFieldType() {
return stringFieldType;
}

public String getStringFieldTypeWithExplicitStringSchemaType() {
return stringFieldTypeWithExplicitStringSchemaType;
}

public String getStringFieldTypeWithExplicitNumberSchemaType() {
return stringFieldTypeWithExplicitNumberSchemaType;
}

public String getStringFieldTypeWithExplicitIntegerSchemaType() {
return stringFieldTypeWithExplicitIntegerSchemaType;
}

public BigDecimal getBigDecimalFieldTypeWithExplicitStringSchemaType() {
return bigDecimalFieldTypeWithExplicitStringSchemaType;
}

public BigDecimal getBigDecimalFieldType() {
return bigDecimalFieldType;
}

}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
package io.swagger.v3.core.issues;

import com.fasterxml.jackson.databind.node.IntNode;
import com.fasterxml.jackson.databind.node.NullNode;
import io.swagger.v3.core.converter.AnnotatedType;
import io.swagger.v3.core.converter.ModelConverterContextImpl;
import io.swagger.v3.core.jackson.ModelResolver;
Expand Down Expand Up @@ -514,8 +516,8 @@ public void testNonNullableIntegerWithNullExampleAndDefault_OAS31() {
(io.swagger.v3.oas.models.media.Schema) model.getProperties().get("integerField");
assertNotNull(integerField, "integerField property should exist");

assertEquals(integerField.getExample(), "null",
"Example should be the string \"null\", not null value");
assertEquals(integerField.getExample(), NullNode.getInstance(),
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

With the addition of support for example/default "null", it was stated that a null should only occur if the field is also nullable. But I would assume that that is not the case here when the schema is a Number schema, and that the correct behavior is instead to retain the null as a JsonNode like the change here. Otherwise the value will not align with the type.

Ignoring null altogether could also be an option for this scenario. But I would need someone to state the expected behavior then. I could also implement so that default and example share their definition, since it seems that that should actually be the case? But that is for a separate PR most likely.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, I think retaining null is a good approach here I think

"Example should be the NullNode \"null\", not null value");
assertEquals(integerField.getDefault(), "null",
"Default should be the string \"null\", not null value");
}
Expand Down Expand Up @@ -610,9 +612,9 @@ public void testNullableIntegerWithMultipleExamplesIncludingNull_OAS31() {
assertNotNull(integerField.getExamples(), "examples array should exist");
assertEquals(integerField.getExamples().size(), 3, "examples array should have 3 elements");

assertTrue(integerField.getExamples().contains("1"), "examples should contain '1' as string");
assertTrue(integerField.getExamples().contains(IntNode.valueOf(1)), "examples should contain 1 as number");
assertTrue(integerField.getExamples().contains(null), "examples should contain null value");
assertTrue(integerField.getExamples().contains("100"), "examples should contain '100' as string");
assertTrue(integerField.getExamples().contains(IntNode.valueOf(100)), "examples should contain 100 as number");
}

/**
Expand Down
Loading
Loading