-
Notifications
You must be signed in to change notification settings - Fork 3
Description
Title: Refactor Java SDK Models to Standardize Jackson Usage via ZitadelModel Base Class
Description:
This initiative will refactor the Java SDK's model classes, which already use Jackson, to standardize their implementation, remove boilerplate, and centralize serialization/deserialization logic within a common ZitadelModel base class.
Problem:
While the Java models use Jackson, the current setup likely involves:
- Boilerplate code within each model (potentially custom
equals,hashCode,toString, validation logic that could be handled by annotations). - Serialization/deserialization logic potentially scattered, often residing primarily within a central
ApiClientorObjectSerializerclass rather than being easily accessible from the model instances themselves. - Lack of a common base class to share the configured
ObjectMapperand provide consistent ser/des helper methods directly on models.
Impact:
Refactoring will result in:
- Cleaner and more maintainable model classes, focusing on property definitions and Jackson annotations.
- Centralized and consistent Jackson
ObjectMapperconfiguration. - Easier-to-use models with self-contained serialization/deserialization capabilities.
- Reduced risk of inconsistencies in how models are handled across the SDK.
Solution / Tasks:
1. Implement ZitadelModel Base Class:
Create an abstract class ZitadelModel. This class will hold a static, shared, pre-configured Jackson ObjectMapper instance and provide convenience methods for serialization and deserialization.
Example ZitadelModel.java (ensure necessary imports):
package com.zitadel.client.model; // Or your chosen SDK package
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.*;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
import org.openapitools.jackson.nullable.JsonNullableModule; // Assuming usage
import java.io.IOException;
import java.text.DateFormat; // If using ApiClient.buildDefaultDateFormat()
import java.util.Map;
public abstract class ZitadelModel {
private static final ObjectMapper defaultObjectMapper = buildObjectMapper();
private static ObjectMapper buildObjectMapper() {
ObjectMapper objectMapper = new ObjectMapper();
// Apply existing configurations (crucial)
objectMapper.setSerializationInclusion(JsonInclude.Include.NON_NULL);
objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
objectMapper.configure(DeserializationFeature.FAIL_ON_INVALID_SUBTYPE, false);
objectMapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
objectMapper.enable(SerializationFeature.WRITE_ENUMS_USING_TO_STRING);
objectMapper.enable(DeserializationFeature.READ_ENUMS_USING_TO_STRING);
objectMapper.registerModule(new JavaTimeModule());
objectMapper.registerModule(new JsonNullableModule()); // Keep if JsonNullable is used
// If ApiClient.buildDefaultDateFormat() exists and is needed for java.util.Date:
// objectMapper.setDateFormat(ApiClient.buildDefaultDateFormat());
// Add any other necessary configuration
return objectMapper;
}
/**
* Gets the shared ObjectMapper instance.
* @return ObjectMapper
*/
public static ObjectMapper getObjectMapper() {
return defaultObjectMapper;
}
/**
* Serializes the current object instance to a JSON string.
* @return JSON string representation
* @throws JsonProcessingException if serialization fails
*/
public String toJson() throws JsonProcessingException {
return getObjectMapper().writeValueAsString(this);
}
/**
* Serializes the current object instance to a Map.
* @return Map representation
*/
@SuppressWarnings("unchecked")
public Map<String, Object> toMap() {
// Convert object to Map using Jackson's convertValue
return getObjectMapper().convertValue(this, Map.class);
}
/**
* Deserializes a JSON string into an instance of the specified class.
* @param jsonString The JSON string
* @param valueType The class of the object to deserialize into
* @param <T> The type of the object
* @return Deserialized object
* @throws IOException if deserialization fails
*/
public static <T extends ZitadelModel> T fromJson(String jsonString, Class<T> valueType) throws IOException {
return getObjectMapper().readValue(jsonString, valueType);
}
/**
* Deserializes a Map into an instance of the specified class.
* @param map The Map
* @param valueType The class of the object to deserialize into
* @param <T> The type of the object
* @return Deserialized object
*/
public static <T extends ZitadelModel> T fromMap(Map<String, Object> map, Class<T> valueType) {
// Convert Map to object using Jackson's convertValue
return getObjectMapper().convertValue(map, valueType);
}
// Consider overriding equals, hashCode, and toString here if a standard implementation is desired
// Or rely on generated implementations using annotations (@JsonIncludeProperties, Lombok, etc.)
}2. Update OpenAPI Generator Templates for Java Models:
Modify the OpenAPI Generator templates for Java models to:
- Make generated models extend
ZitadelModel. - Use standard Java types and Jackson annotations for defining properties.
- Use
@JsonProperty("jsonKey")to map JSON keys to Java field names where they differ (e.g., JSON"userId"vs. JavauserIdoruserId_). Jackson'sPropertyNamingStrategiescan also be configured on theObjectMapperfor global conversion (e.g.,CAMEL_CASE_TO_LOWER_CASE_WITH_UNDERSCORES). - Remove any custom serialization/deserialization logic previously embedded in the model or its helpers.
- Ensure enums use
@JsonValueor rely on theWRITE_ENUMS_USING_TO_STRING/READ_ENUMS_USING_TO_STRINGconfiguration. - Leverage
@JsonInclude(JsonInclude.Include.NON_NULL)or other Jackson annotations for fine-grained control if needed (though the global setting often suffices). - Use
org.openapitools.jackson.nullable.JsonNullable<T>wrapper for fields that need to explicitly distinguish betweennullandunset, if required by the API design (especially for PATCH).
Target structure for a generated model (e.g., UserServiceUser.java):
package com.zitadel.client.model; // Or your chosen SDK package
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.annotation.JsonTypeName; // If needed for discriminators later
import java.util.List;
import java.util.Objects;
// Assuming other models (UserServiceDetails etc.) also extend ZitadelModel
// Assuming UserServiceUserState is an Enum configured for Jackson
// Add other necessary annotations like @JsonIncludeProperties, @Generated, etc. if used
public class UserServiceUser extends ZitadelModel {
@JsonProperty("userId")
private String userId;
@JsonProperty("details")
private UserServiceDetails details;
@JsonProperty("state")
private UserServiceUserState state; // Assuming enum
@JsonProperty("username")
private String username;
@JsonProperty("loginNames")
private List<String> loginNames;
@JsonProperty("preferredLoginName")
private String preferredLoginName;
@JsonProperty("human")
private UserServiceHumanUser human;
@JsonProperty("machine")
private UserServiceMachineUser machine;
// Constructors (e.g., default, all-args if needed)
public UserServiceUser() {
// Default constructor
}
// Getters and Setters (standard JavaBeans pattern)
public String getUserId() { return userId; }
public void setUserId(String userId) { this.userId = userId; }
// ... other getters/setters ...
// Consider standard equals, hashCode, toString (can be generated by IDEs or Lombok)
@Override
public boolean equals(Object o) {
// Standard implementation...
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
UserServiceUser that = (UserServiceUser) o;
return Objects.equals(userId, that.userId) && Objects.equals(details, that.details) /* && etc. */;
}
@Override
public int hashCode() {
return Objects.hash(userId, details /* , etc. */);
}
@Override
public String toString() {
// Standard implementation or use Jackson via toJson()
try {
return toJson(); // Or a simpler format
} catch (JsonProcessingException e) {
return "UserServiceUser{serialization error}";
}
}
}3. Update SDK Code to Use New Model Methods:
- Identify where the
ApiClient(or equivalent) currently uses itsobjectMapper.writeValueAsString()andobjectMapper.readValue()methods specifically for model objects. - Replace these calls with the new methods from
ZitadelModel:objectMapper.writeValueAsString(instance)->instance.toJson().objectMapper.readValue(data, ModelClass.class)->ModelClass.fromJson(jsonData, ModelClass.class).objectMapper.readValue(data, new TypeReference<List<ModelClass>>(){})-> Use Jackson'sObjectMapperwithTypeFactoryor handle list deserialization appropriately, potentially keeping this logic in the client but ensuring it uses the sharedObjectMapperfromZitadelModel.getObjectMapper().
- The aim is to use the model's own ser/des capabilities where appropriate and ensure the shared
ObjectMapperis used consistently.
Expected Outcomes:
- Java SDK models extend
ZitadelModeland primarily contain field definitions and standard Java methods (getters/setters, equals, hashCode, toString). - JSON serialization and deserialization logic is centralized in
ZitadelModelusing a shared, configured JacksonObjectMapper. - Boilerplate related to custom serialization within models is removed.
- The SDK is easier to maintain and aligns better with standard Java/Jackson practices.
- Functional equivalence for JSON-based API interactions is preserved.
Additional Notes:
- Dependencies: Ensure
jackson-databind,jackson-datatype-jsr310(for Java Time), andjackson-databind-nullable(if usingJsonNullable) are correctly managed (e.g., via Maven or Gradle). - Null Handling: The base
ObjectMapperis configured withJsonInclude.Include.NON_NULL. Use@JsonIncludeon specific fields/classes if different behavior is needed.JsonNullable<T>should be used for fields requiring explicit null vs. unset distinction. - Testing: Thorough testing of serialization/deserialization for various model types is essential.
- Discriminators: Functionality related to discriminators (
@JsonTypeInfo,@JsonSubTypes) is out of scope but should be compatible with this structure if added later.