Skip to content

Commit

Permalink
Adding support for a serializedName attribute. (#41)
Browse files Browse the repository at this point in the history
* Adding support for a serializedName attribute.

The serializedName attribute defines the member name to use when serializing to JSON. This allows for vaid JSON member names that cannot be represented as Java field names.
  • Loading branch information
malaysf authored Jun 5, 2023
1 parent ac5568d commit e62a888
Show file tree
Hide file tree
Showing 6 changed files with 251 additions and 31 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

## [Unreleased] - TBD
### Added
* Added the `serializedName` annotation to rename the serialized Json member name
* Added support for AtomicLongArray in B2Json
* Reduced lock contention in B2Json
* Updated internal python for building to python3
Expand Down
34 changes: 34 additions & 0 deletions core/src/main/java/com/backblaze/b2/json/B2Json.java
Original file line number Diff line number Diff line change
Expand Up @@ -572,6 +572,40 @@ public <T> T fromUrlParameterMap(Map<String, String> parameterMap, Class<T> claz
@Target(ElementType.FIELD)
public @interface sensitive {}

/**
* Annotation to declare that this member will be serialized to JSON
* with the specified name, instead of the field name in the Java class.
*
* The Java class's field name is used for the params list in the
* B2Json.constructor annotation
*
* For example:
* <pre>
* class Example {
* {@literal @}B2Json.serializedName(value = "@field")
* private String field;
*
* {@literal @}B2Json.constructor(params = "field")
* public Example(String field) {
* this.field = field;
* }
* }
* </pre>
* will serialize to the following JSON:
* <pre>
* {
* "@field": "value"
* }
* </pre>
*
*/
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
public @interface serializedName {
String value();
}


/**
* Constructor annotation saying that this is the constructor B2Json
* should use. This constructor must take ALL of the serializable
Expand Down
55 changes: 34 additions & 21 deletions core/src/main/java/com/backblaze/b2/json/B2JsonObjectHandler.java
Original file line number Diff line number Diff line change
Expand Up @@ -53,9 +53,14 @@ public class B2JsonObjectHandler<T> extends B2JsonTypeHandlerWithDefaults<T> {
private FieldInfo [] fields;

/**
* Map from field name to field.
* Map from json member name to FieldInfo.
*/
private final Map<String, FieldInfo> fieldMap = new HashMap<>();
private final Map<String, FieldInfo> jsonMemberNameFieldInfoMap = new HashMap<>();

/**
* Map from Java object field name to FieldInfo
*/
private final Map<String, FieldInfo> javaFieldNameFieldInfoMap = new HashMap<>();

/**
* The constructor to use.
Expand Down Expand Up @@ -129,10 +134,18 @@ protected void initializeImplementation(B2JsonHandlerMap handlerMap) throws B2Js
final VersionRange versionRange = getVersionRange(field);
final boolean isSensitive = field.getAnnotation(B2Json.sensitive.class) != null;
final boolean omitNull = omitNull(field);
final FieldInfo fieldInfo = new FieldInfo(field, handler, requirement, defaultValueJsonOrNull, versionRange, isSensitive, omitNull);
fieldMap.put(field.getName(), fieldInfo);
final B2Json.serializedName serializedNameAnnotation = field.getAnnotation(B2Json.serializedName.class);
final String jsonMemberName = serializedNameAnnotation != null ? serializedNameAnnotation.value() : field.getName();
final FieldInfo fieldInfo =
new FieldInfo(jsonMemberName, field, handler, requirement, defaultValueJsonOrNull, versionRange, isSensitive, omitNull);

if (jsonMemberNameFieldInfoMap.containsKey(jsonMemberName)) {
throw new B2JsonException(clazz.getName() + " contains multiple class fields for the json member " + jsonMemberName);
}
jsonMemberNameFieldInfoMap.put(jsonMemberName, fieldInfo);
javaFieldNameFieldInfoMap.put(field.getName(), fieldInfo);
}
fields = fieldMap.values().toArray(new FieldInfo [fieldMap.size()]);
fields = jsonMemberNameFieldInfoMap.values().toArray(new FieldInfo [jsonMemberNameFieldInfoMap.size()]);
Arrays.sort(fields);

// Find the constructor to use.
Expand Down Expand Up @@ -202,7 +215,7 @@ protected void initializeImplementation(B2JsonHandlerMap handlerMap) throws B2Js
versionParamIndex = i;
}
else {
final FieldInfo fieldInfo = fieldMap.get(paramName);
final FieldInfo fieldInfo = javaFieldNameFieldInfoMap.get(paramName);
if (fieldInfo == null) {
throw new B2JsonException(clazz.getName() + " param name is not a field: " + paramName);
}
Expand All @@ -222,7 +235,7 @@ protected void initializeImplementation(B2JsonHandlerMap handlerMap) throws B2Js
String[] discardNames = discardsWithCommas.split(",");
fieldsToDiscard = B2Collections.unmodifiableSet(discardNames);
for (String name : fieldsToDiscard) {
final FieldInfo fieldInfo = fieldMap.get(name);
final FieldInfo fieldInfo = javaFieldNameFieldInfoMap.get(name);
if (fieldInfo != null && fieldInfo.requirement != FieldRequirement.IGNORED) {
throw new B2JsonException(clazz.getSimpleName() + "'s field '" + name + "' cannot be discarded: it's " + fieldInfo.requirement + ". only non-existent or IGNORED fields can be discarded.");
}
Expand Down Expand Up @@ -269,7 +282,7 @@ private boolean omitNull(Field field) throws B2JsonException {
*/
@Override
protected void checkDefaultValues() throws B2JsonException {
for (FieldInfo field : fieldMap.values()) {
for (FieldInfo field : jsonMemberNameFieldInfoMap.values()) {
if (field.defaultValueJsonOrNull != null) {
try {
field.handler.deserialize(
Expand All @@ -278,7 +291,7 @@ protected void checkDefaultValues() throws B2JsonException {
);
} catch (B2JsonException | IOException e) {
throw new B2JsonException("error in default value for " +
clazz.getSimpleName() + "." + field.getName() + ": " +
clazz.getSimpleName() + "." + field.getJsonMemberName() + ": " +
e.getMessage());
}
}
Expand Down Expand Up @@ -350,7 +363,7 @@ public void serialize(T obj, B2JsonOptions options, B2JsonWriter out) throws IOE
out.startObject();
if (fields != null) {
for (FieldInfo fieldInfo : fields) {
if (unionTypeFieldName != null && !typeFieldDone && unionTypeFieldName.compareTo(fieldInfo.getName()) < 0) {
if (unionTypeFieldName != null && !typeFieldDone && unionTypeFieldName.compareTo(fieldInfo.getJsonMemberName()) < 0) {
out.writeObjectFieldNameAndColon(unionTypeFieldName);
out.writeString(unionTypeFieldValue);
typeFieldDone = true;
Expand All @@ -360,12 +373,12 @@ public void serialize(T obj, B2JsonOptions options, B2JsonWriter out) throws IOE

// Only write the field if the value is not null OR omitNull is not set
if (!fieldInfo.omitNull || value != null) {
out.writeObjectFieldNameAndColon(fieldInfo.getName());
out.writeObjectFieldNameAndColon(fieldInfo.getJsonMemberName());
if (fieldInfo.getIsSensitive() && options.getRedactSensitive()) {
out.writeString("***REDACTED***");
} else {
if (fieldInfo.isRequiredAndInVersion(version) && value == null) {
throw new B2JsonException("required field " + fieldInfo.getName() + " cannot be null");
throw new B2JsonException("required field " + fieldInfo.getJsonMemberName() + " cannot be null");
}
//noinspection unchecked
B2JsonUtil.serializeMaybeNull(fieldInfo.handler, value, out, options);
Expand Down Expand Up @@ -410,7 +423,7 @@ public T deserialize(B2JsonReader in, B2JsonOptions options) throws B2JsonExcept
if (in.startObjectAndCheckForContents()) {
do {
String fieldName = in.readObjectFieldNameAndColon();
FieldInfo fieldInfo = fieldMap.get(fieldName);
FieldInfo fieldInfo = jsonMemberNameFieldInfoMap.get(fieldName);
if (fieldInfo == null) {
if ((options.getExtraFieldOption() == B2JsonOptions.ExtraFieldOption.ERROR) &&
(fieldsToDiscard == null || !fieldsToDiscard.contains(fieldName))) {
Expand All @@ -420,12 +433,12 @@ public T deserialize(B2JsonReader in, B2JsonOptions options) throws B2JsonExcept
}
else {
if (foundFieldBits.get(fieldInfo.constructorArgIndex)) {
throw new B2JsonException("duplicate field: " + fieldInfo.getName());
throw new B2JsonException("duplicate field: " + fieldInfo.getJsonMemberName());
}
@SuppressWarnings("unchecked")
final Object value = B2JsonUtil.deserializeMaybeNull(fieldInfo.handler, in, options);
if (fieldInfo.isRequiredAndInVersion(version) && value == null) {
throw new B2JsonException("required field " + fieldInfo.getName() + " cannot be null");
throw new B2JsonException("required field " + fieldInfo.getJsonMemberName() + " cannot be null");
}
constructorArgs[fieldInfo.constructorArgIndex] = value;
foundFieldBits.set(fieldInfo.constructorArgIndex);
Expand Down Expand Up @@ -456,7 +469,7 @@ public T deserializeFromFieldNameToValueMap(Map<String, Object> fieldNameToValue
}
for (Map.Entry<String, Object> entry : fieldNameToValue.entrySet()) {
String fieldName = entry.getKey();
FieldInfo fieldInfo = fieldMap.get(fieldName);
FieldInfo fieldInfo = jsonMemberNameFieldInfoMap.get(fieldName);
if (fieldInfo == null) {
if ((options.getExtraFieldOption() == B2JsonOptions.ExtraFieldOption.ERROR) &&
(fieldsToDiscard == null || !fieldsToDiscard.contains(fieldName))) {
Expand All @@ -466,7 +479,7 @@ public T deserializeFromFieldNameToValueMap(Map<String, Object> fieldNameToValue
else {
Object value = entry.getValue();
if (fieldInfo.isRequiredAndInVersion(version) && value == null) {
throw new B2JsonException("required field " + fieldInfo.getName() + " cannot be null");
throw new B2JsonException("required field " + fieldInfo.getJsonMemberName() + " cannot be null");
}
constructorArgs[fieldInfo.constructorArgIndex] = value;
}
Expand All @@ -490,7 +503,7 @@ public T deserializeFromUrlParameterMap(Map<String, String> parameterMap, B2Json
String fieldName = entry.getKey();
String strOfValue = entry.getValue();

FieldInfo fieldInfo = fieldMap.get(fieldName);
FieldInfo fieldInfo = jsonMemberNameFieldInfoMap.get(fieldName);
if (fieldInfo == null) {
if ((options.getExtraFieldOption() == B2JsonOptions.ExtraFieldOption.ERROR) &&
(fieldsToDiscard == null || !fieldsToDiscard.contains(fieldName))) {
Expand All @@ -500,7 +513,7 @@ public T deserializeFromUrlParameterMap(Map<String, String> parameterMap, B2Json
else {
final Object value = fieldInfo.handler.deserializeUrlParam(strOfValue);
if (fieldInfo.isRequiredAndInVersion(version) && value == null) {
throw new B2JsonException("required field " + fieldInfo.getName() + " cannot be null");
throw new B2JsonException("required field " + fieldInfo.getJsonMemberName() + " cannot be null");
}
constructorArgs[fieldInfo.constructorArgIndex] = value;
}
Expand All @@ -522,7 +535,7 @@ private T deserializeFromConstructorArgs(Object[] constructorArgs, int version)
int index = fieldInfo.constructorArgIndex;
if (constructorArgs[index] == null) {
if (fieldInfo.isRequiredAndInVersion(version)) {
throw new B2JsonException("required field " + fieldInfo.getName() + " is missing");
throw new B2JsonException("required field " + fieldInfo.getJsonMemberName() + " is missing");
}
if (fieldInfo.defaultValueJsonOrNull != null) {
// We do a fresh deserialization of the default value each time, in case it's
Expand All @@ -548,7 +561,7 @@ private T deserializeFromConstructorArgs(Object[] constructorArgs, int version)
}
else {
if (!fieldInfo.isInVersion(version)) {
throw new B2JsonException("field " + fieldInfo.getName() + " is not in version " + version);
throw new B2JsonException("field " + fieldInfo.getJsonMemberName() + " is not in version " + version);
}
}
}
Expand Down
21 changes: 17 additions & 4 deletions core/src/main/java/com/backblaze/b2/json/FieldInfo.java
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,6 @@

package com.backblaze.b2.json;

import com.backblaze.b2.util.B2Preconditions;

import java.lang.reflect.Field;

/**
Expand All @@ -18,6 +16,7 @@ public final class FieldInfo implements Comparable<FieldInfo> {

public enum FieldRequirement { REQUIRED, OPTIONAL, IGNORED }

private final String jsonMemberName;
public final Field field;
public final B2JsonTypeHandler handler;
public final FieldRequirement requirement;
Expand All @@ -28,13 +27,15 @@ public enum FieldRequirement { REQUIRED, OPTIONAL, IGNORED }
public final boolean omitNull;

/*package*/ FieldInfo(
String jsonMemberName,
Field field, B2JsonTypeHandler<?> handler,
FieldRequirement requirement,
String defaultValueJsonOrNull,
VersionRange versionRange,
boolean isSensitive,
boolean omitNull
) {
this.jsonMemberName = jsonMemberName;
this.field = field;
this.handler = handler;
this.requirement = requirement;
Expand All @@ -46,8 +47,20 @@ public enum FieldRequirement { REQUIRED, OPTIONAL, IGNORED }
this.field.setAccessible(true);
}

/**
* Returns the member name that this field is serialized to in Json.
* @deprecated use {@link #getJsonMemberName()} instead which is clearer.
*/
@Deprecated
public String getName() {
return field.getName();
return jsonMemberName;
}

/**
* Returns the member name that this field is serialized to in Json.
*/
public String getJsonMemberName() {
return jsonMemberName;
}

public B2JsonTypeHandler getHandler() {
Expand All @@ -59,7 +72,7 @@ public boolean getIsSensitive() {
}

public int compareTo(@SuppressWarnings("NullableProblems") FieldInfo o) {
return field.getName().compareTo(o.field.getName());
return jsonMemberName.compareTo(o.jsonMemberName);
}

public void setConstructorArgIndex(int index) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,13 @@ public void testDeserializeWithMismatchingParamOrder() throws B2JsonException {
MismatchingOrderContainer.class);
}

@Test
public void testSeralizedFieldName() {
String json = "{\"b\": 41}";
final ContainerWithDifferentserializedName obj = B2Json.fromJsonOrThrowRuntime(json, ContainerWithDifferentserializedName.class);

assertEquals(41, obj.a);
}

private static class Empty {
@B2Json.constructor Empty() {}
Expand Down Expand Up @@ -178,6 +185,17 @@ public boolean equals(Object o) {
}
}

private static class ContainerWithDifferentserializedName {
@B2Json.required
@B2Json.serializedName(value = "b")
public int a;

@B2Json.constructor
public ContainerWithDifferentserializedName(int a) {
this.a = a;
}
}

private static final B2Json b2Json = B2Json.get();

}
Loading

0 comments on commit e62a888

Please sign in to comment.