Netflix / dgs-codegen

Apache License 2.0
182 stars 99 forks source link

Support Custom Serialization for Date Fields in InputValueSerializer #691

Open bishnoisnb29 opened 4 months ago

bishnoisnb29 commented 4 months ago

Issue Description

The current implementation of the InputValueSerializer in the DGS Codegen library does not support custom serialization for Date fields. This limitation makes it challenging to handle Date objects properly when constructing GraphQL queries, especially when dates need to be serialized to ISO 8601 format.

Steps to Reproduce

  1. Create a GraphQL query request that includes Date fields in the input.
  2. Attempt to serialize the query using GraphQLQueryRequest.serialize().
  3. Observe that Date fields are not converted to ISO 8601 format.

Expected Behavior

The InputValueSerializer should provide a way to handle custom serialization for Date fields, allowing dates to be converted to ISO 8601 strings during the serialization process.

Current Workaround

Currently, users need to preprocess the input map manually before creating the GraphQLQueryRequest. This involves traversing the input map and converting Date objects to ISO 8601 strings using a utility method. While effective, this approach adds additional complexity and is not ideal.

Example of the Current Workaround

import com.fasterxml.jackson.databind.util.StdDateFormat;

import java.util.Date;
import java.util.HashMap;
import java.util.Map;

public class GraphQLUtil {

    private static final StdDateFormat dateFormat = new StdDateFormat();

    public static Map<String, Object> preprocessInput(Map<String, Object> input) {
        Map<String, Object> processedInput = new HashMap<>();
        for (Map.Entry<String, Object> entry : input.entrySet()) {
            Object value = entry.getValue();
            if (value instanceof Date) {
                processedInput.put(entry.getKey(), dateFormat.format((Date) value));
            } else {
                processedInput.put(entry.getKey(), value);
            }
        }
        return processedInput;
    }
}
import java.util.HashMap;
import java.util.Map;

public final class GraphQLQueryRequest {
    @NotNull
    private final GraphQLQuery query;
    @Nullable
    private final BaseProjectionNode projection;
    @NotNull
    private final InputValueSerializer inputValueSerializer;
    @NotNull
    private final ProjectionSerializer projectionSerializer;

    public GraphQLQueryRequest(@NotNull GraphQLQuery query, @Nullable BaseProjectionNode projection, @Nullable Map<Class<?>, ? extends Coercing<?, ?>> scalars) {
        Intrinsics.checkNotNullParameter(query, "query");
        super();
        this.query = query;
        this.projection = projection;
        this.inputValueSerializer = new InputValueSerializer(scalars != null ? scalars : MapsKt.emptyMap());
        this.projectionSerializer = new ProjectionSerializer(this.inputValueSerializer);
    }

    // Additional constructor to handle only query and projection
    public GraphQLQueryRequest(@NotNull GraphQLQuery query, @Nullable BaseProjectionNode projection) {
        this(query, projection, null);
    }

    @NotNull
    public final GraphQLQuery getQuery() {
        return this.query;
    }

    @Nullable
    public final BaseProjectionNode getProjection() {
        return this.projection;
    }

    @NotNull
    public final InputValueSerializer getInputValueSerializer() {
        return this.inputValueSerializer;
    }

    @NotNull
    public final ProjectionSerializer getProjectionSerializer() {
        return this.projectionSerializer;
    }

    @NotNull
    public final String serialize() {
        OperationDefinition.Builder operationDef = OperationDefinition.newOperationDefinition();
        String queryName = this.query.getName();
        if (queryName != null) {
            operationDef.name(queryName);
        }

        String operationType = this.query.getOperationType();
        if (operationType != null) {
            operationDef.operation(Operation.valueOf(operationType.toUpperCase(Locale.ROOT)));
        }

        if (!this.query.getVariableDefinitions().isEmpty()) {
            operationDef.variableDefinitions(this.query.getVariableDefinitions());
        }

        Field.Builder selection = Field.newField(this.query.getOperationName());
        if (!this.query.getInput().isEmpty()) {
            Map<String, Object> inputMap = this.query.getInput();
            Map<String, Object> processedInputMap = GraphQLUtil.preprocessInput(inputMap);
            List<Argument> arguments = new ArrayList<>();
            for (Map.Entry<String, Object> entry : processedInputMap.entrySet()) {
                arguments.add(new Argument(entry.getKey(), this.inputValueSerializer.toValue(entry.getValue())));
            }
            selection.arguments(arguments);
        }

        if (this.projection != null) {
            SelectionSet selectionSet = this.projection instanceof BaseSubProjectionNode
                ? this.projectionSerializer.toSelectionSet(((BaseSubProjectionNode) this.projection).root())
                : this.projectionSerializer.toSelectionSet(this.projection);
            if (!selectionSet.getSelections().isEmpty()) {
                selection.selectionSet(selectionSet);
            }
        }

        operationDef.selectionSet(SelectionSet.newSelectionSet().selection(selection.build()).build());
        return AstPrinter.printAst(operationDef.build());
    }
}

Proposed Solution

Enhance the InputValueSerializer to support custom serializers for specific types, including Date. This could be achieved by allowing users to register custom serializers for different classes, which would then be used during the serialization process.

Additional Context

Adding support for custom serializers will improve the flexibility and usability of the DGS Codegen library, making it easier to work with various data types, including Date, in a more seamless and intuitive manner.

pfaul commented 1 month ago

I am having similar troubles serializing and deserializing java.time.YearMonth and org.joda.money.Money.

Is there any update on this topic?

EDIT: It would be very nice to just register (or pass through) a class to handle the de/serialization, for example jakarta.json.bind.Jsonb