Skip to content

Support attributes for xml examples #698

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
Merged
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
2 changes: 2 additions & 0 deletions dependencies.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,8 @@ ext {
jakartaAnnotationApiVersion = '3.0.0'
jakartaValidationApiVersion = '3.0.2'

jakartaXmlBindApiVersion = '4.0.2'

jsonSchemaValidator = '1.3.3'

mockitoCoreVersion = '5.11.0'
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,22 @@
@RequiredArgsConstructor
public class DefaultSchemaWalker<T, R> implements SchemaWalker<R> {

Boolean DEFAULT_BOOLEAN_EXAMPLE = true;

String DEFAULT_STRING_EXAMPLE = "string";
Integer DEFAULT_INTEGER_EXAMPLE = 0;
Double DEFAULT_NUMBER_EXAMPLE = 1.1;

String DEFAULT_DATE_EXAMPLE = "2015-07-20";
String DEFAULT_DATE_TIME_EXAMPLE = "2015-07-20T15:49:04-07:00";
String DEFAULT_PASSWORD_EXAMPLE = "string-password";
String DEFAULT_BYTE_EXAMPLE = "YmFzZTY0LWV4YW1wbGU=";
String DEFAULT_BINARY_EXAMPLE =
"0111010001100101011100110111010000101101011000100110100101101110011000010110010001111001";

String DEFAULT_EMAIL_EXAMPLE = "[email protected]";
String DEFAULT_UUID_EXAMPLE = "3fa85f64-5717-4562-b3fc-2c963f66afa6";

private final ExampleValueGenerator<T, R> exampleValueGenerator;

@Override
Expand All @@ -32,7 +48,8 @@ public R fromSchema(Schema schema, Map<String, Schema> definitions) {

String schemaName = exampleValueGenerator.lookupSchemaName(schema);
try {
T generatedExample = buildExample(schemaName, schema, definitions, new HashSet<>());
T generatedExample = buildExample(schemaName, schema, definitions, new HashSet<>())
.orElseThrow(() -> new ExampleGeneratingException("Something went wrong"));

return exampleValueGenerator.prepareForSerialization(schema, generatedExample);
} catch (ExampleGeneratingException ex) {
Expand All @@ -41,9 +58,9 @@ public R fromSchema(Schema schema, Map<String, Schema> definitions) {
return null;
}

private T buildExample(String name, Schema schema, Map<String, Schema> definitions, Set<Schema> visited) {
T exampleValue = getExampleValueFromSchemaAnnotation(schema);
if (exampleValue != null) {
private Optional<T> buildExample(String name, Schema schema, Map<String, Schema> definitions, Set<Schema> visited) {
Optional<T> exampleValue = getExampleValueFromSchemaAnnotation(schema);
if (exampleValue.isPresent()) {
return exampleValue;
}

Expand All @@ -55,96 +72,98 @@ private T buildExample(String name, Schema schema, Map<String, Schema> definitio
String type = schema.getType();
return switch (type) {
case "array" -> buildArrayExample(schema, definitions, visited);
case "boolean" -> exampleValueGenerator.createBooleanExample();
case "integer" -> exampleValueGenerator.createIntegerExample();
case "number" -> exampleValueGenerator.createDoubleExample();
case "boolean" -> exampleValueGenerator.createBooleanExample(DEFAULT_BOOLEAN_EXAMPLE, schema);
case "integer" -> exampleValueGenerator.createIntegerExample(DEFAULT_INTEGER_EXAMPLE, schema);
case "number" -> exampleValueGenerator.createDoubleExample(DEFAULT_NUMBER_EXAMPLE, schema);
case "object" -> buildFromObjectSchema(name, schema, definitions, visited);
case "string" -> buildFromStringSchema(schema);
default -> exampleValueGenerator.createUnknownSchemaStringTypeExample(type);
};
}

private T getExampleValueFromSchemaAnnotation(Schema schema) {
private Optional<T> getExampleValueFromSchemaAnnotation(Schema schema) {
Object exampleValue = schema.getExample();

// schema is a map of properties from a nested object, whose example cannot be inferred
if (exampleValue == null) {
return null;
return Optional.empty();
}

// Return directly, when we have processed this before
T processedExample = exampleValueGenerator.getExampleOrNull(schema, exampleValue);
if (processedExample != null) {
return processedExample;
return Optional.of(processedExample);
}

// Handle special types (i.e. map) with custom @Schema annotation and specified example value
Object additionalProperties = schema.getAdditionalProperties();
if (additionalProperties instanceof StringSchema additionalPropertiesSchema) {
Object exampleValueString = additionalPropertiesSchema.getExample();
if (exampleValueString != null) {
return exampleValueGenerator.createRaw(exampleValueString);
return Optional.ofNullable(exampleValueGenerator.createRaw(exampleValueString));
}
}

// schema is a map of properties from a nested object, whose example cannot be inferred
if (exampleValue instanceof Map) {
return null;
return Optional.empty();
}

// value is represented in their native type
if (exampleValue instanceof Boolean) {
return exampleValueGenerator.createBooleanExample((Boolean) exampleValue);
return exampleValueGenerator.createBooleanExample((Boolean) exampleValue, schema);
} else if (exampleValue instanceof Number) {
double doubleValue = ((Number) exampleValue).doubleValue();

// Check if it's an integer (whole number)
if (doubleValue == (int) doubleValue) {
return exampleValueGenerator.createIntegerExample((int) doubleValue);
return exampleValueGenerator.createIntegerExample((int) doubleValue, schema);
}

return exampleValueGenerator.createDoubleExample(doubleValue);
return exampleValueGenerator.createDoubleExample(doubleValue, schema);
}

try {
// value (i.e. OffsetDateTime) is represented as string
return exampleValueGenerator.createStringExample(exampleValue.toString());
return exampleValueGenerator.createStringExample(exampleValue.toString(), schema);
} catch (IllegalArgumentException ex) {
log.debug("Unable to convert example to JSON: %s".formatted(exampleValue.toString()), ex);
}

return exampleValueGenerator.createEmptyObjectExample();
}

private T buildArrayExample(Schema schema, Map<String, Schema> definitions, Set<Schema> visited) {
private Optional<T> buildArrayExample(Schema schema, Map<String, Schema> definitions, Set<Schema> visited) {
Schema arrayItemSchema =
resolveSchemaFromRef(schema.getItems(), definitions).orElse(schema.getItems());
String arrayItemName = exampleValueGenerator.lookupSchemaName(arrayItemSchema);
T arrayItem = buildExample(arrayItemName, arrayItemSchema, definitions, visited);

Optional<T> arrayItem = buildExample(arrayItemName, arrayItemSchema, definitions, visited);

String arrayName = exampleValueGenerator.lookupSchemaName(schema);
return exampleValueGenerator.createArrayExample(arrayName, arrayItem);

return arrayItem.map(array -> exampleValueGenerator.createArrayExample(arrayName, array));
}

private T buildFromStringSchema(Schema schema) {
private Optional<T> buildFromStringSchema(Schema schema) {
String firstEnumValue = getFirstEnumValue(schema);
if (firstEnumValue != null) {
return exampleValueGenerator.createEnumExample(firstEnumValue);
return exampleValueGenerator.createEnumExample(firstEnumValue, schema);
}

String format = schema.getFormat();
if (format == null) {
return exampleValueGenerator.createStringExample();
return exampleValueGenerator.createStringExample(DEFAULT_STRING_EXAMPLE, schema);
}

return switch (format) {
case "date" -> exampleValueGenerator.createDateExample();
case "date-time" -> exampleValueGenerator.createDateTimeExample();
case "email" -> exampleValueGenerator.createEmailExample();
case "password" -> exampleValueGenerator.createPasswordExample();
case "byte" -> exampleValueGenerator.createByteExample();
case "binary" -> exampleValueGenerator.createBinaryExample();
case "uuid" -> exampleValueGenerator.createUuidExample();
case "date" -> exampleValueGenerator.createStringExample(DEFAULT_DATE_EXAMPLE, schema);
case "date-time" -> exampleValueGenerator.createStringExample(DEFAULT_DATE_TIME_EXAMPLE, schema);
case "email" -> exampleValueGenerator.createStringExample(DEFAULT_EMAIL_EXAMPLE, schema);
case "password" -> exampleValueGenerator.createStringExample(DEFAULT_PASSWORD_EXAMPLE, schema);
case "byte" -> exampleValueGenerator.createStringExample(DEFAULT_BYTE_EXAMPLE, schema);
case "binary" -> exampleValueGenerator.createStringExample(DEFAULT_BINARY_EXAMPLE, schema);
case "uuid" -> exampleValueGenerator.createStringExample(DEFAULT_UUID_EXAMPLE, schema);
default -> exampleValueGenerator.createUnknownSchemaStringFormatExample(format);
};
}
Expand All @@ -160,27 +179,37 @@ private String getFirstEnumValue(Schema schema) {
return null;
}

private T buildFromObjectSchema(String name, Schema schema, Map<String, Schema> definitions, Set<Schema> visited) {
private Optional<T> buildFromObjectSchema(
String name, Schema schema, Map<String, Schema> definitions, Set<Schema> visited) {
Map<String, Schema> properties = schema.getProperties();
if (properties != null) {
if (!visited.contains(schema)) {
visited.add(schema);
if (properties != null && !visited.contains(schema)) {

List<PropertyExample<T>> propertyList =
buildPropertyExampleListFromSchema(properties, definitions, visited);
T example = exampleValueGenerator.createObjectExample(name, propertyList);
visited.add(schema);

visited.remove(schema);
return example;
}
T object = exampleValueGenerator.startObject(name);

List<PropertyExample<T>> propertyList =
buildPropertyExampleListFromSchema(properties, definitions, visited);
exampleValueGenerator.addPropertyExamples(object, propertyList);

exampleValueGenerator.endObject();

visited.remove(schema);
return Optional.of(object);
}

if (schema.getAllOf() != null && !schema.getAllOf().isEmpty()) {
List<Schema> schemas = schema.getAllOf();

T object = exampleValueGenerator.startObject(name);

List<PropertyExample<T>> mergedProperties =
buildPropertyExampleListFromSchemas(schemas, definitions, visited);
return exampleValueGenerator.createObjectExample(name, mergedProperties);
exampleValueGenerator.addPropertyExamples(object, mergedProperties);

exampleValueGenerator.endObject();

return Optional.of(object);
}
if (schema.getAnyOf() != null && !schema.getAnyOf().isEmpty()) {
List<Schema> schemas = schema.getAnyOf();
Expand All @@ -200,11 +229,16 @@ private T buildFromObjectSchema(String name, Schema schema, Map<String, Schema>
private List<PropertyExample<T>> buildPropertyExampleListFromSchema(
Map<String, Schema> properties, Map<String, Schema> definitions, Set<Schema> visited) {
return properties.entrySet().stream()
.map(entry -> {
String propertyKey = entry.getKey();
T propertyValue = buildExample(propertyKey, entry.getValue(), definitions, visited);
return new PropertyExample<>(propertyKey, propertyValue);
.map(propertySchema -> {
String propertyKey = propertySchema.getKey();
Optional<T> propertyValue =
buildExample(propertyKey, propertySchema.getValue(), definitions, visited);

return propertyValue
.map(optionalElem -> new PropertyExample<>(propertyKey, optionalElem))
.orElse(null);
})
.filter(Objects::nonNull)
.sorted(Comparator.comparing(PropertyExample::name))
.toList();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import io.swagger.v3.oas.models.media.Schema;

import java.util.List;
import java.util.Optional;

/**
* Provides the building blocks to generate an example
Expand All @@ -13,28 +14,12 @@
*/
public interface ExampleValueGenerator<T, R> {

Boolean DEFAULT_BOOLEAN_EXAMPLE = true;

String DEFAULT_STRING_EXAMPLE = "string";
Integer DEFAULT_INTEGER_EXAMPLE = 0;
Double DEFAULT_NUMBER_EXAMPLE = 1.1;

String DEFAULT_DATE_EXAMPLE = "2015-07-20";
String DEFAULT_DATE_TIME_EXAMPLE = "2015-07-20T15:49:04-07:00";
String DEFAULT_PASSWORD_EXAMPLE = "string-password";
String DEFAULT_BYTE_EXAMPLE = "YmFzZTY0LWV4YW1wbGU=";
String DEFAULT_BINARY_EXAMPLE =
"0111010001100101011100110111010000101101011000100110100101101110011000010110010001111001";

String DEFAULT_EMAIL_EXAMPLE = "[email protected]";
String DEFAULT_UUID_EXAMPLE = "3fa85f64-5717-4562-b3fc-2c963f66afa6";

boolean canHandle(String contentType);

/**
* Some internal representation need to be initialized per Schema
*/
void initialize();
default void initialize() {}

String lookupSchemaName(Schema schema);

Expand All @@ -43,45 +28,27 @@ public interface ExampleValueGenerator<T, R> {
*/
R prepareForSerialization(Schema name, T exampleObject);

T createIntegerExample(Integer value);

T createDoubleExample(Double value);

T createBooleanExample();

T createBooleanExample(Boolean value);

T createIntegerExample();

T createObjectExample(String name, List<PropertyExample<T>> properties);

T createEmptyObjectExample();

T createDoubleExample();

T createDateExample();

T createDateTimeExample();
Optional<T> createIntegerExample(Integer value, Schema schema);

T createEmailExample();
Optional<T> createDoubleExample(Double value, Schema schema);

T createPasswordExample();
Optional<T> createBooleanExample(Boolean value, Schema schema);

T createByteExample();
Optional<T> createEmptyObjectExample();

T createBinaryExample();
Optional<T> createStringExample(String value, Schema schema);

T createUuidExample();
Optional<T> createEnumExample(String anEnumValue, Schema schema);

T createStringExample();
Optional<T> createUnknownSchemaStringTypeExample(String schemaType);

T createStringExample(String value);
Optional<T> createUnknownSchemaStringFormatExample(String schemaFormat);

T createEnumExample(String anEnumValue);
T startObject(String name);

T createUnknownSchemaStringTypeExample(String schemaType);
default void endObject() {}

T createUnknownSchemaStringFormatExample(String schemaFormat);
void addPropertyExamples(T object, List<PropertyExample<T>> properties);

T createArrayExample(String name, T arrayItem);

Expand Down
Loading