Skip to content
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

Basic support for Jackson's @JsonUnwrapped annotation (2.0.x) #677

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
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ public class JacksonConstants {
.createSimple("com.fasterxml.jackson.annotation.JsonIgnoreProperties");
public static final DotName JSON_PROPERTY_ORDER = DotName
.createSimple("com.fasterxml.jackson.annotation.JsonPropertyOrder");
public static final DotName JSON_UNWRAPPED = DotName
.createSimple("com.fasterxml.jackson.annotation.JsonUnwrapped");

public static final String PROP_VALUE = "value";

Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package io.smallrye.openapi.runtime.scanner.dataobject;

import java.lang.reflect.Modifier;
import java.util.AbstractMap.SimpleEntry;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Arrays;
Expand Down Expand Up @@ -57,6 +58,8 @@ public class TypeResolver {
private boolean writeOnly = false;
private Type leaf;
private List<AnnotationTarget> constraintTargets = new ArrayList<>();
private String propertyNamePrefix;
private String propertyNameSuffix;

/**
* A comparator to order the field, write method, and read method in the {@link #targets}
Expand Down Expand Up @@ -190,22 +193,37 @@ public String getPropertyName() {
if ((name = TypeUtil.getAnnotationValue(target,
SchemaConstant.DOTNAME_SCHEMA,
SchemaConstant.PROP_NAME)) != null) {
return name;
return wrap(name);
}

if ((name = TypeUtil.getAnnotationValue(target,
JsonbConstants.JSONB_PROPERTY,
JsonbConstants.PROP_VALUE)) != null) {
return name;
return wrap(name);
}

if ((name = TypeUtil.getAnnotationValue(target,
JacksonConstants.JSON_PROPERTY,
JacksonConstants.PROP_VALUE)) != null) {
return name;
return wrap(name);
}

return this.propertyName;
return wrap(this.propertyName);
}

private String wrap(String name) {
if (this.propertyNamePrefix == null && this.propertyNameSuffix == null) {
return name;
}
StringBuilder wrapped = new StringBuilder();
if (this.propertyNamePrefix != null) {
wrapped.append(this.propertyNamePrefix);
}
wrapped.append(name);
if (this.propertyNameSuffix != null) {
wrapped.append(this.propertyNameSuffix);
}
return wrapped.toString();
}

public String getBeanPropertyName() {
Expand Down Expand Up @@ -379,7 +397,7 @@ public static Map<String, TypeResolver> getAllFields(AugmentedIndexView index, I
currentClass.fields()
.stream()
.filter(TypeResolver::acceptField)
.forEach(field -> scanField(properties, field, stack, reference, ignoreResolver));
.forEach(field -> scanField(index, properties, field, stack, reference, ignoreResolver));

currentClass.methods()
.stream()
Expand Down Expand Up @@ -510,11 +528,22 @@ boolean isUnhidden(AnnotationTarget target) {
* @param reference an annotated member (field or method) that referenced the type of field's declaring class
* @param ignoreResolver resolver to determine if the field is ignored
*/
private static void scanField(Map<String, TypeResolver> properties, FieldInfo field, Deque<Map<String, Type>> stack,
private static void scanField(AugmentedIndexView index, Map<String, TypeResolver> properties, FieldInfo field,
Deque<Map<String, Type>> stack,
AnnotationTarget reference, IgnoreResolver ignoreResolver) {
String propertyName = field.name();
final String propertyName = field.name();
final Type fieldType = field.type();
final ClassInfo fieldClass = index.getClass(fieldType);
final boolean unwrapped;
final TypeResolver resolver;

if (field.hasAnnotation(JacksonConstants.JSON_UNWRAPPED) && fieldClass != null) {
unwrapped = true;
properties.putAll(unwrapProperties(index, field, fieldType, fieldClass, ignoreResolver));
} else {
unwrapped = false;
}

// Consider only using fields that are public?
if (properties.containsKey(propertyName)) {
resolver = properties.get(propertyName);
Expand All @@ -535,7 +564,49 @@ private static void scanField(Map<String, TypeResolver> properties, FieldInfo fi
resolver.constraintTargets.add(field);
}

resolver.processVisibility(field, reference, ignoreResolver);
if (unwrapped) {
// Ignored for getters/setters
resolver.ignored = true;
} else {
resolver.processVisibility(field, reference, ignoreResolver);
}
}

private static Map<String, TypeResolver> unwrapProperties(AugmentedIndexView index,
AnnotationTarget member,
Type memberType,
ClassInfo memberClass,
IgnoreResolver ignoreResolver) {

Map<String, TypeResolver> unwrappedProperties = getAllFields(index, ignoreResolver, memberType, memberClass, member);
AnnotationInstance jsonUnwrapped = TypeUtil.getAnnotation(member, JacksonConstants.JSON_UNWRAPPED);
String unwrapPrefix = JandexUtil.value(jsonUnwrapped, "prefix");
String unwrapSuffix = JandexUtil.value(jsonUnwrapped, "suffix");

return unwrappedProperties.entrySet()
.stream()
.map(p -> applyPrefixSuffix(p, unwrapPrefix, unwrapSuffix))
.collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
}

static Map.Entry<String, TypeResolver> applyPrefixSuffix(Map.Entry<String, TypeResolver> property, String prefix,
String suffix) {
TypeResolver unwrappedResolver = property.getValue();
StringBuilder key = new StringBuilder();

if (prefix != null) {
unwrappedResolver.propertyNamePrefix = prefix;
key.append(prefix);
}

key.append(property.getKey());

if (suffix != null) {
unwrappedResolver.propertyNameSuffix = suffix;
key.append(suffix);
}

return new SimpleEntry<>(key.toString(), unwrappedResolver);
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@
import org.json.JSONException;
import org.junit.Test;

import com.fasterxml.jackson.annotation.JsonUnwrapped;

public class StandaloneSchemaScanTest extends IndexScannerTestBase {

@Test
Expand Down Expand Up @@ -275,4 +277,54 @@ static class JAXBElementDto {
@XmlElementRef(name = "CaseSubtitleFree", namespace = "urn:Milo.API.Miljo.DataContracts.V1", type = JAXBElement.class, required = false)
protected JAXBElement<String> caseSubtitleFree;
}

/****************************************************************/

/*
* https://github.com/smallrye/smallrye-open-api/issues/226
*/
@Test
public void testJacksonJsonUnwrapped() throws IOException, JSONException {
Index index = indexOf(JacksonJsonPerson.class, JacksonJsonPersonWithPrefixedAddress.class,
JacksonJsonPersonWithSuffixedAddress.class, JacksonJsonAddress.class);
OpenApiAnnotationScanner scanner = new OpenApiAnnotationScanner(emptyConfig(), index);
OpenAPI result = scanner.scan();
printToConsole(result);
assertJsonEquals("components.schemas-jackson-jsonunwrapped.json", result);
}

@Schema
static class JacksonJsonPerson {
protected String name;
@JsonUnwrapped
protected JacksonJsonAddress address;

@Schema(description = "Ignored since address is unwrapped")
public JacksonJsonAddress getAddress() {
return address;
}
}

@Schema
static class JacksonJsonPersonWithPrefixedAddress {
protected String name;
@JsonUnwrapped(prefix = "addr-")
protected JacksonJsonAddress address;
}

@Schema
static class JacksonJsonPersonWithSuffixedAddress {
protected String name;
@JsonUnwrapped(suffix = "-addr")
protected JacksonJsonAddress address;
}

@Schema
static class JacksonJsonAddress {
protected int streetNumber;
protected String streetName;
protected String city;
protected String state;
protected String postalCode;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
{
"openapi": "3.0.3",
"components": {
"schemas": {
"JacksonJsonPerson": {
"type": "object",
"properties": {
"streetName": {
"type": "string"
},
"streetNumber": {
"format": "int32",
"type": "integer"
},
"city": {
"type": "string"
},
"postalCode": {
"type": "string"
},
"state": {
"type": "string"
},
"name": {
"type": "string"
}
}
},
"JacksonJsonPersonWithPrefixedAddress": {
"type": "object",
"properties": {
"addr-city": {
"type": "string"
},
"addr-postalCode": {
"type": "string"
},
"addr-streetName": {
"type": "string"
},
"addr-state": {
"type": "string"
},
"addr-streetNumber": {
"format": "int32",
"type": "integer"
},
"name": {
"type": "string"
}
}
},
"JacksonJsonPersonWithSuffixedAddress": {
"type": "object",
"properties": {
"city-addr": {
"type": "string"
},
"streetName-addr": {
"type": "string"
},
"postalCode-addr": {
"type": "string"
},
"streetNumber-addr": {
"format": "int32",
"type": "integer"
},
"state-addr": {
"type": "string"
},
"name": {
"type": "string"
}
}
},
"JacksonJsonAddress": {
"type": "object",
"properties": {
"city": {
"type": "string"
},
"postalCode": {
"type": "string"
},
"state": {
"type": "string"
},
"streetName": {
"type": "string"
},
"streetNumber": {
"format": "int32",
"type": "integer"
}
}
}
}
}
}