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 @@ -62,6 +62,9 @@ public void setFailOnUnknownProperties(boolean failOnUnknownProperties) {
throwException();
}

@Override
public void setFailOnRequiredProperties(boolean failOnRequiredProperties) { throwException(); }

@Override
public void setPrettyResponseLoggingEnabled(boolean prettyResponseLoggingEnabled) {
throwException();
Expand Down Expand Up @@ -248,6 +251,11 @@ public void setLibraryMaintainerMode(boolean libraryMaintainerMode) {
*/
private boolean failOnUnknownProperties = false;

/**
* If you would like to detect required properties by throwing exceptions, set this flag as true.
*/
private boolean failOnRequiredProperties = false;

/**
* Slack Web API client verifies the existence of tokens before sending HTTP requests to Slack servers.
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,12 +44,17 @@ public static Gson createSnakeCase(SlackConfig config) {
GsonBuilder gsonBuilder = new GsonBuilder()
.setFieldNamingPolicy(FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES);
registerTypeAdapters(gsonBuilder, failOnUnknownProps);

if (failOnUnknownProps || config.isLibraryMaintainerMode()) {
gsonBuilder = gsonBuilder.registerTypeAdapterFactory(new UnknownPropertyDetectionAdapterFactory());
Copy link
Author

Choose a reason for hiding this comment

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

This reassignment is redundant, so removing it

gsonBuilder.registerTypeAdapterFactory(new UnknownPropertyDetectionAdapterFactory());
}
if (config.isFailOnRequiredProperties()) {
gsonBuilder.registerTypeAdapterFactory(new RequiredPropertyDetectionAdapterFactory());
}
if (config.isPrettyResponseLoggingEnabled()) {
gsonBuilder = gsonBuilder.setPrettyPrinting();
Copy link
Author

@fst-john fst-john Feb 26, 2026

Choose a reason for hiding this comment

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

Same thing here - there's no need for the re-assignmen

gsonBuilder.setPrettyPrinting();
}

return gsonBuilder.create();
}

Expand All @@ -60,12 +65,17 @@ public static Gson createCamelCase(SlackConfig config) {
boolean failOnUnknownProps = config.isFailOnUnknownProperties();
GsonBuilder gsonBuilder = new GsonBuilder();
registerTypeAdapters(gsonBuilder, failOnUnknownProps);

if (failOnUnknownProps || config.isLibraryMaintainerMode()) {
gsonBuilder = gsonBuilder.registerTypeAdapterFactory(new UnknownPropertyDetectionAdapterFactory());
gsonBuilder.registerTypeAdapterFactory(new UnknownPropertyDetectionAdapterFactory());
}
if (config.isFailOnRequiredProperties()) {
gsonBuilder.registerTypeAdapterFactory(new RequiredPropertyDetectionAdapterFactory());
}
if (config.isPrettyResponseLoggingEnabled()) {
gsonBuilder = gsonBuilder.setPrettyPrinting();
gsonBuilder.setPrettyPrinting();
}

return gsonBuilder.create();
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package com.slack.api.model.annotation;

import com.slack.api.model.predicate.FieldPredicate;
import com.slack.api.model.predicate.IsNotNullFieldPredicate;
import com.slack.api.util.json.RequiredPropertyDetectionAdapterFactory;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/**
* Field-level annotation indicating whether the field is a "required" field or not on the model object.
* <p>
* The enforcement of the field's presence in instantiated instances of the model object is accomplished using the
* {@link RequiredPropertyDetectionAdapterFactory} which ensures all fields marked with {@link Required} are
* present during the object deserialization (or serialization) process. Note that the enforcement of this annotation
* is opt-in and defaults to "off".
*/
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
public @interface Required {
/**
* Optional predicate to evaluate against the field annotated with {@link Required}. By default, all fields
* marked with {@link Required} are checked for null. Primitive field types are initialized by the JVM, and thus
* are never null by default.
*/
Class<? extends FieldPredicate> validator() default IsNotNullFieldPredicate.class;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package com.slack.api.model.predicate;

/**
* A functional interface for defining validation predicates against {@link java.lang.reflect.Field}. Used by
* {@link com.slack.api.model.annotation.Required} during object serialization and deserialization to ensure the
* field member is "valid" per the defined predicate.
*/
@FunctionalInterface
public interface FieldPredicate {
boolean validate(Object obj);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package com.slack.api.model.predicate;

import java.util.Objects;

public class IsNotNullFieldPredicate implements FieldPredicate {
@Override
public boolean validate(Object obj) {
return Objects.nonNull(obj);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
package com.slack.api.util.json;

import com.google.gson.JsonParseException;
import com.google.gson.Gson;
import com.google.gson.TypeAdapterFactory;
import com.google.gson.TypeAdapter;
import com.google.gson.reflect.TypeToken;
import com.google.gson.stream.JsonReader;
import com.google.gson.stream.JsonWriter;
import com.slack.api.model.predicate.FieldPredicate;
import com.slack.api.model.annotation.Required;
import lombok.EqualsAndHashCode;
import lombok.RequiredArgsConstructor;

import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.io.IOException;

/**
* Adapter factory for processing objects annotated with {@link Required}. This annotation signals what properties
* of a model object are required, and thus should be expected to be initialized on instantiated instances. For all
* fields on the model objected annotated with {@link Required} applies the {@link FieldPredicate#test(Object)} via the
* specified {@link Required#validator()}.
* <p>
* Note that this adapter handles both deserialization (JSON --> POJO) and serialization (POJO --> JSON).
*/
public class RequiredPropertyDetectionAdapterFactory implements TypeAdapterFactory {
@Override
public <T> TypeAdapter<T> create(Gson gson, TypeToken<T> type) {
TypeAdapter<T> delegate = gson.getDelegateAdapter(this, type);
List<RequiredFieldEntry> entries = buildRequiredFieldEntries(type.getRawType());

if (entries.isEmpty()) {
return delegate;
}

return new TypeAdapter<T>() {
@Override
public void write(JsonWriter out, T value) throws IOException {
if (value != null) {
ensureFieldValidity(value, entries);
}
delegate.write(out, value);
}

@Override
public T read(JsonReader in) throws IOException {
T result = delegate.read(in);
if (result == null) {
return null;
}

ensureFieldValidity(result, entries);
return result;
}
};
}

/**
* Scans the given class for fields annotated with {@link Required}, pre-resolves each field's
* accessibility and {@link FieldPredicate} instance, and returns an immutable list of entries.
* This is called once per type during Gson adapter creation.
*/
private List<RequiredFieldEntry> buildRequiredFieldEntries(Class<?> clazz) {
List<RequiredFieldEntry> entries = new ArrayList<>();
for (Field field : clazz.getDeclaredFields()) {
Required annotation = field.getAnnotation(Required.class);
if (annotation != null) {
field.setAccessible(true);
try {
FieldPredicate predicate = annotation.validator().getDeclaredConstructor().newInstance();
entries.add(new RequiredFieldEntry(field, predicate));
} catch (NoSuchMethodException | InstantiationException |
IllegalAccessException | InvocationTargetException e) {
throw new JsonParseException(
"Cannot instantiate validator for field: " + field.getName(), e);
}
}
}
return Collections.unmodifiableList(entries);
}

private <T> void ensureFieldValidity(T obj, List<RequiredFieldEntry> entries) {
for (RequiredFieldEntry entry : entries) {
try {
Object value = entry.field.get(obj);
if (!entry.predicate.validate(value)) {
throw new JsonParseException("Required field '" + entry.field.getName()
+ "' failed validation in " + obj.getClass().getSimpleName()
+ " using predicate " + entry.predicate.getClass().getSimpleName());
}
} catch (IllegalAccessException e) {
throw new JsonParseException(
"Cannot access field: " + entry.field.getName(), e);
}
}
}

/**
* Class holding the accessible {@link Field} handle and the cached instance of {@link FieldPredicate}.
*/
@RequiredArgsConstructor
@EqualsAndHashCode
private static class RequiredFieldEntry {
final Field field;
final FieldPredicate predicate;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,19 @@ public static Gson createSnakeCaseWithoutUnknownPropertyDetection(boolean failOn
return createSnakeCase(failOnUnknownProperties, false);
}

public static Gson createSnakeCaseWithRequiredPropertyDetection() {
return createSnakeCase(false, true, true);
}

public static Gson createSnakeCase(boolean failOnUnknownProperties, boolean unknownPropertyDetection) {
return createSnakeCase(failOnUnknownProperties, unknownPropertyDetection, false);
}

public static Gson createSnakeCase(
boolean failOnUnknownProperties,
boolean unknownPropertyDetection,
boolean failOnRequiredProperties
) {
GsonBuilder builder = new GsonBuilder()
.setFieldNamingPolicy(FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES)
.registerTypeAdapter(File.class, new GsonFileFactory(failOnUnknownProperties))
Expand All @@ -45,9 +57,12 @@ public static Gson createSnakeCase(boolean failOnUnknownProperties, boolean unkn
new GsonMessageChangedEventPreviousMessageFactory(failOnUnknownProperties));

if (unknownPropertyDetection) {
return builder.registerTypeAdapterFactory(new UnknownPropertyDetectionAdapterFactory()).create();
} else {
return builder.create();
builder.registerTypeAdapterFactory(new UnknownPropertyDetectionAdapterFactory());
}
if (failOnRequiredProperties) {
builder.registerTypeAdapterFactory(new RequiredPropertyDetectionAdapterFactory());
}

return builder.create();
}
}
Loading
Loading