Hi @markus-async-dune,
The operator type name is declared via @QueryOperator("exact_match") (an A12 annotation Jackson doesn’t know about) so a plain ObjectMapper falls back to the simple class name "ExactMatchOperator". You need to register the @QueryOperator-annotated classes as Jackson NamedTypes on your client-side ObjectMapper (the A12 server does the same thing internally at startup).
1. What’s happening
ILogicOperator declares polymorphic type info as @JsonTypeInfo(use = Id.NAME, property = "operator", defaultImpl = UnknownOperator.class). With Id.NAME, Jackson resolves the discriminator value from @JsonTypeName or from registered subtypes. ExactMatchOperator carries @QueryOperator("exact_match") instead — an A12-specific annotation Jackson doesn’t see — so it falls back to the simple class name.
2. Register operator subtypes on your ObjectMapper
Option A — explicit (fine if you only use a handful of operators):
objectMapper.registerSubtypes(
new NamedType(ExactMatchOperator.class, "exact_match"),
new NamedType(AndOperator.class, "and"),
new NamedType(OrOperator.class, "or")
// … any other operators you use
);
Option B — scan like the server does (recommended; picks up every operator without you maintaining a list). This is exactly what DefaultQueryGeneratorContext does on the server using org.reflections:reflections:
Reflections reflections = new Reflections("com.mgmtp.a12.dataservices");
reflections.getTypesAnnotatedWith(QueryOperator.class).stream()
.filter(c -> !Modifier.isAbstract(c.getModifiers()))
.map(c -> new NamedType(c, c.getAnnotation(QueryOperator.class).value()))
.forEach(objectMapper::registerSubtypes);
If you also use aggregation functions, repeat the scan for @QueryAggregationFunction — the server does that in the same method.
3. Apply it to both directions
Register subtypes on the ObjectMapper you use for both serialization and deserialization. Otherwise polymorphic reads will silently fall back to UnknownOperator (the defaultImpl on ILogicOperator) and you’ll lose constraint info on the way back.
Hint: If you prefer not to hand-roll the JSON-RPC envelope, RequestBuilderFactory.newJsonRpc2RequestBuilder() from dataservices-client gives you a typed builder; it still delegates to the injected ObjectMapper, so the registration above remains the prerequisite.
Further reading:
com.mgmtp.a12.dataservices.query.constraint.ILogicOperator — the @JsonTypeInfo declaration and the @JsonIgnore getOperator() default.
com.mgmtp.a12.dataservices.query.generator.sql.internal.DefaultQueryGeneratorContext — the reference reflection-based registration used by the server.
Cheers,
Markus