Skip to content

Type-safe client custom serialiser for input variable #2257

@jmini

Description

@jmini

I am using the gitlab graphql api and I am running this mutation request:

mutation workItemUpdate($arg0: WorkItemUpdateInput!) {
  workItemUpdate(input: $arg0) {
    errors
    workItem {
      id
      webUrl
    }
  }
}

The java client I am using is here:
https://github.com/unblu/gitlab-workitem-graphql-client/

In particular see the WorkItemUpdateInput class definition which has a member hierarchyWidget of type WorkItemWidgetHierarchyUpdateInput

The documentation is not really clear about the hierarchyWidget:
https://docs.gitlab.com/ee/api/graphql/reference/#workitemwidgethierarchyupdateinput

Those are the key point (for the client):

  • for parentId you have 3 values:
    • setting an id (String value) to set a new parent
    • setting null to remove the parent association
    • not setting the attribute to not modify the parent association
  • you can not set parentId and childrenIds in the same request
    • runtime error: "One and only one of children, parent or remove_child is required", so in theory we should do multiple runs, especially in case of "turning a child into a parent or the other way around".

So again this is a case where not setting the value and setting it explicitly to null has a different meaning.
json-schema-org/json-schema-spec#584 (comment)

And in Java this is always tricky.


I stared an implementation with a NullableProperty<T> helper class (to support the 3 states) annotated with @JsonbTypeSerializer and @JsonbTypeDeserializer

Yasson test (jbang)

///usr/bin/env jbang "$0" "$@" ; exit $?

//DEPS org.eclipse:yasson:3.0.4
//JAVA 11

import java.lang.reflect.Type;
import java.util.Optional;

import jakarta.json.JsonValue;
import jakarta.json.bind.Jsonb;
import jakarta.json.bind.JsonbBuilder;
import jakarta.json.bind.annotation.JsonbTypeDeserializer;
import jakarta.json.bind.annotation.JsonbTypeSerializer;
import jakarta.json.bind.serializer.DeserializationContext;
import jakarta.json.bind.serializer.JsonbDeserializer;
import jakarta.json.bind.serializer.JsonbSerializer;
import jakarta.json.bind.serializer.SerializationContext;
import jakarta.json.stream.JsonGenerator;
import jakarta.json.stream.JsonParser;

public class YassonTest {

    public static void main(String[] args) {
        Jsonb jsonb = JsonbBuilder.create();

        Example example = new Example();

        // Case 1: Set name to "John"
        example.setName(NullableProperty.of("John"));
        System.out.println(jsonb.toJson(example)); // {"name":"John"}

        // Case 2: Set name to NullableProperty.empty()
        example.setName(NullableProperty.empty());
        System.out.println(jsonb.toJson(example)); // {"name":null}

        // Case 3: Set name to null
        example.setName(null);
        System.out.println(jsonb.toJson(example)); // {}

    }

    public static class Example {
        @JsonbTypeSerializer(NullablePropertySerializer.class)
        @JsonbTypeDeserializer(NullablePropertyDeserializer.class)
        private NullableProperty<String> name;

        public NullableProperty<String> getName() {
            return name;
        }

        public void setName(NullableProperty<String> name) {
            this.name = name;
        }
    }

    public static class NullablePropertyDeserializer<T> implements JsonbDeserializer<NullableProperty<T>> {

        @Override
        public NullableProperty<T> deserialize(JsonParser parser, DeserializationContext ctx, Type rtType) {
            JsonValue value = parser.getValue();
            if (value == JsonValue.NULL) {
                return NullableProperty.empty();
            } else {
                T deserializedValue = ctx.deserialize(rtType, parser);
                return NullableProperty.of(deserializedValue);
            }
        }
    }

    public static class NullablePropertySerializer<T> implements JsonbSerializer<NullableProperty<T>> {
        @Override
        public void serialize(NullableProperty<T> obj, JsonGenerator generator, SerializationContext ctx) {
            if (obj.isPresent()) {
                ctx.serialize(obj.get(), generator);
            } else {
                generator.write(JsonValue.NULL);
            }
        }
    }

    public static class NullableProperty<T> {
        private final T value;

        private NullableProperty(T value) {
            this.value = value;
        }

        public static <T> NullableProperty<T> of(T value) {
            if (value == null) {
                throw new IllegalArgumentException("Use NullableProperty.empty() for null values");
            }
            return new NullableProperty<>(value);
        }

        public static <T> NullableProperty<T> empty() {
            return new NullableProperty<>(null);
        }

        public Optional<T> toOptional() {
            return Optional.ofNullable(value);
        }

        public T get() {
            return value;
        }

        public boolean isPresent() {
            return value != null;
        }
    }
}

It works great at Json-b level, but I have the feeling that the typesafe client is doing something else when it comes to the serialization of the Input object.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions