简体   繁体   中英

Graphql SPQR customize object serialization / deserialization

I have the following data model with custom attributes:

    class Foo {
      private Long id;
      private Set<AdditionalAttribute> attributes;
    }
    class AdditionalAttribute {
      private Key key;
      private String value;
    }
    class Key {
        private String name;
        private Class<?> type;
    }

My model produces this json:

{"id":123, "attributes": [{"key1":12345}, {"key2":"value2"}]}

My expected json is:

{"id":123, "key1":12345, "key2":"value2"}

How can I achieve a such serialization / deserialization using graphql spqr?

FYI, currently I can do it in REST API with jackson (BeanSerializerModifier for serialization and @JsonAnySetter for deserialization) as follow:

    // Serialization using BeanSerializerModifier
    class FooModifier extends BeanSerializerModifier {

        @Override
        public List<BeanPropertyWriter> changeProperties(
              SerializationConfig config, BeanDescription beanDesc,
              List<BeanPropertyWriter> beanProperties) {
            for (int i = 0; i < beanProperties.size(); i++) {
                BeanPropertyWriter writer = beanProperties.get(i);
                if (Foo.class.isAssignableFrom(beanDesc.getBeanClass()) && "attributes".equals(writer.getName())) {
                    beanProperties.set(i, new FooAttributesWriter(writer));
                }
            }
            return beanProperties;
        }
    }
    class FooAttributesWriter extends BeanPropertyWriter {
        public HasAttributesWriter(BeanPropertyWriter w) {
            super(w);
        }
        @Override
        public void serializeAsField(Object bean, JsonGenerator gen,
                                     SerializerProvider prov) throws Exception {
            if(Foo.class.isAssignableFrom(bean.getClass())) {
                Set<AdditionalAttribute> set = ((Foo) bean).getAttributes();
                for (AdditionalAttribute a : set) {
                    gen.writeStringField(a.getKey().getName(), a.getValue());
                }
            }
        }
    }

    // Deserilization using @JsonAnySetter
    class Foo {
        private Long id;
        private Set<AdditionalAttribute> attributes;
        // Deserialization of custom properties
        @JsonAnySetter
        public void set(String name, Object value) {
            attributes.add(new AdditionalAttribute(buildKey(name,value), value));
        }
    }

The problem here is not JSON (de)serialization. With GraphQL, the shape of all your inputs and outputs is defined by the schema, and the schema can not normally have dynamic parts (object types where the fields are unknown ahead of time). Because your Set<AdditionalAttribute> can contain anything at all at runtime, it means your Foo type would have to have unknown fields. This is highly antithetical to how GraphQL is designed.

The only way to achieve a dynamic structure is to have an object scalar which effectively is a JSON blob that can not be validated, or sub-selected from. You could turn Foo into such a scalar by adding @GraphQLScalar to it. Then all input would be valid, {"id":123, "key1":12345 "key2":"value2"} but also {"whatever": "something"} . And it would be your logic's job to ensure correctness. Additionally, if you ever return Foo , the client would not be able to sub-select from it. Eg {foo} would be possible but {foo { id }} would not, because the schema would no longer know if the id field is present.

To recap, you options are:

  1. Leaving it as it is (the dynamic stuff is a list nested under attributes )
  2. Turning Set<AdditionalAttribute> into a type (a new class or EnumMap ) with known structure with all the possible keys as fields. This is only possible if the keys aren't totally dynamic
  3. Making the whole enclosing object an object scalar by using @GraphQLScalar

Thanks a lot for your time and the proposed options. Currently, we have found another way (maybe option 4 :) ) to generate a "similar" json to the expected output (We lost the type information in the generated output, but we have another logic that helps us to retrieve the type).

Here an example :

class Foo {
      private Long id;
      private Set<AdditionalAttribute> attributes;
      @GraphQLQuery
      public String get(@GraphQLArgument(name = "key") String key) {
        for (AdditionalAttribute a : attributes) {
            if (a.getConfigurationKey().getKey().equalsIgnoreCase(key)) {
                return a.getAttributeValue();
            }
        }
        return null;
    }

and we can sub-select Foo as follow:

foo {
   id
   key1: get(key: "key1")
   key2: get(key: "key2")
}

And this return

{"id":123, "key1":"12345", "key2":"value2"}

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM