Skip to content

Fixed the messy auto-generated serde logic in the library #44

@mridang

Description

@mridang

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 ApiClient or ObjectSerializer class rather than being easily accessible from the model instances themselves.
  • Lack of a common base class to share the configured ObjectMapper and 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 ObjectMapper configuration.
  • 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. Java userId or userId_). Jackson's PropertyNamingStrategies can also be configured on the ObjectMapper for 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 @JsonValue or rely on the WRITE_ENUMS_USING_TO_STRING / READ_ENUMS_USING_TO_STRING configuration.
  • 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 between null and unset, 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 its objectMapper.writeValueAsString() and objectMapper.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's ObjectMapper with TypeFactory or handle list deserialization appropriately, potentially keeping this logic in the client but ensuring it uses the shared ObjectMapper from ZitadelModel.getObjectMapper().
  • The aim is to use the model's own ser/des capabilities where appropriate and ensure the shared ObjectMapper is used consistently.

Expected Outcomes:

  • Java SDK models extend ZitadelModel and primarily contain field definitions and standard Java methods (getters/setters, equals, hashCode, toString).
  • JSON serialization and deserialization logic is centralized in ZitadelModel using a shared, configured Jackson ObjectMapper.
  • 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), and jackson-databind-nullable (if using JsonNullable) are correctly managed (e.g., via Maven or Gradle).
  • Null Handling: The base ObjectMapper is configured with JsonInclude.Include.NON_NULL. Use @JsonInclude on 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.

Metadata

Metadata

Assignees

Labels

enhancementNew feature or request

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions