ScaCap / spring-auto-restdocs

Spring Auto REST Docs is an extension to Spring REST Docs
https://scacap.github.io/spring-auto-restdocs/
Apache License 2.0
310 stars 86 forks source link

Custom serializer ignored #334

Closed ChristianSch closed 5 years ago

ChristianSch commented 5 years ago

Hi there,

Thanks for your work!

So my problem is the following: I have a list of objects in several returns, which I serialize with a custom JsonSerializer to a list of their ids. Eg:

    @ToString.Exclude
    @JsonProperty("processingUserIds")
    @JsonSerialize(using = IObjectWithIdListSerializer.class)
    List<User> processingUsers;
public class IObjectWithIdListSerializer extends JsonSerializer<List<IObjectWithId>> {

    @Override
    public void serialize(List<IObjectWithId> objects, JsonGenerator jsonGenerator,
                          SerializerProvider serializerProvider) throws IOException {
        jsonGenerator.writeStartArray();

        for (IObjectWithId o : objects) {
            jsonGenerator.writeObject(o.getId().toString());
        }

        jsonGenerator.writeEndArray();
    }

}

The json response actually contains the properly serialized list of ids, but SARD does not detect the type correctly and has a kind of infinite recursion going on with the actual object.

How would I let SARD know the type, how would I enforce it? If it's not capable of that I'd be glad of any pointers to contribute to this. Cheers.

BTW, this is one row of the table in auto-response-fields.adoc, which might indicate erroneous behaviour when it comes to optionality in this case?

|informationUnit.processingUserIds[].processingInformationUnits[].mandator.refObjectIds[].children[].parent.children[].children[].paymentAccounts[].type
|String
|true +
true +
true +
true +
true +
true +
true +
true +
true +
true +
true +
true +
true +
true +
true +
true +
true +
true +
true +
true +
true +
true +
true +
true +
true +
true +
true +
true +
true +
true +
true +
true +
true +
true +
true +
true +
true +
true +
true +
true +
true +
true +
true +
true +
true +
true +
true +
true +
true +
true +
true +
true +
true +
true +
true +
true +
true +
true +
true +
true +
true +
true +
true +
true +
true +
true +
true +
true +
true +
true +
true +
true
|
fbenz commented 5 years ago

You should be able to control this by overriding acceptJsonFormatVisitor in your custom serializer. An example is https://github.com/ScaCap/spring-auto-restdocs/blob/master/spring-auto-restdocs-core/src/test/java/capital/scalable/restdocs/jackson/BigDecimalSerializer.java

The entry point for walking through the POJOs with Jackson is https://github.com/ScaCap/spring-auto-restdocs/blob/master/spring-auto-restdocs-core/src/main/java/capital/scalable/restdocs/jackson/FieldDocumentationGenerator.java#L79 So ObjectMapper.acceptJsonFormatVisitor is used. Its documentation states

Method for visiting type hierarchy for given type, using specified visitor. Visitation uses Serializer hierarchy and related properties

It only sees the serialization side but this is fine in this case. Please make sure that the provided ObjectMapper needs to have the custom serializer configured (done via .alwaysDo(JacksonResultHandlers.prepareJackson(objectMapper))). The different types (object, array, string, ...) are handled in https://github.com/ScaCap/spring-auto-restdocs/blob/master/spring-auto-restdocs-core/src/main/java/capital/scalable/restdocs/jackson/FieldDocumentationVisitorWrapper.java

ChristianSch commented 5 years ago

Thanks so much! I understood you as follows. I provided the serializer with an implementation of acceptJsonFormatVisitor reflecting the field of a list of strings from the object.

public class IObjectWithIdListSerializer extends JsonSerializer<List<IObjectWithId>> {

    @Override
    public void serialize(List<IObjectWithId> objects, JsonGenerator jsonGenerator,
                          SerializerProvider serializerProvider) throws IOException {
        jsonGenerator.writeStartArray();

        for (IObjectWithId o : objects) {
            jsonGenerator.writeObject(o.getId().toString());
        }

        jsonGenerator.writeEndArray();
    }

    @Override
    public void acceptJsonFormatVisitor(JsonFormatVisitorWrapper visitor, JavaType typeHint)
            throws JsonMappingException {
        if (visitor != null) {
            visitor.expectArrayFormat(typeHint).itemsFormat(JsonFormatTypes.STRING);
        }
    }

}

The objectMapper was already configured.

@Slf4j
@Transactional
@ActiveProfiles("test")
@RunWith(SpringJUnit4ClassRunner.class)
@SpringBootTest(classes = {TestApplication.class}, webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
public abstract class AbstractMvcTest {

    // ...

    @Autowired
    protected ObjectMapper objectMapper;

    @Before
    public void setUp() {
        // ...
        this.mockMvc = MockMvcBuilders.webAppContextSetup(this.wac)
                .addFilters(springSecurityFilterChain)
                .alwaysDo(JacksonResultHandlers.prepareJackson(objectMapper))
                .alwaysDo(commonDocumentation())
                .apply(MockMvcRestDocumentation.documentationConfiguration(restDocumentation)
                        .uris()
                        .withScheme("http")
                        .withHost("localhost")
                        .withPort(8080)
                        .and().snippets()
                        .withDefaults(CliDocumentation.curlRequest(),
                                HttpDocumentation.httpRequest(),
                                HttpDocumentation.httpResponse(),
                                AutoDocumentation.requestFields(),
                                AutoDocumentation.responseFields(),
                                AutoDocumentation.pathParameters(),
                                AutoDocumentation.requestParameters(),
                                AutoDocumentation.description(),
                                AutoDocumentation.methodAndPath(),
                                AutoDocumentation.section()))
                .build();

        // ...
    }

}

However, it did not change the output. Did I misunderstand you or have I missed anything? Thanks.

fbenz commented 5 years ago

I played around with your serializer in the Spring WebMVC example project and it looks like serializers that are added by annotations are not considered (correctly) by Spring Auto REST Docs, e.g. @JsonSerialize(using = IObjectWithIdListSerializer.class)

I tried adding the serializer on the module

JavaType type = mapper.getTypeFactory().
                constructCollectionType(List.class, IObjectWithId.class);
module.addSerializer((Class<List<IObjectWithId>>) type.getRawClass(),
                new IObjectWithIdListSerializer());

but then it was applied to other classes as well and of course caused issues.

Thus, I can only suggest a workaround: Create a IObjectWithIdSerializer

public class IObjectWithIdSerializer extends StdSerializer<IObjectWithId> {

    public IObjectWithIdSerializer() {
        super(IObjectWithId.class);
    }

    @Override
    public void serialize(IObjectWithId object, JsonGenerator jsonGenerator,
            SerializerProvider serializerProvider) throws IOException {
        jsonGenerator.writeObject(object.getId().toString());;
    }

    @Override
    public void acceptJsonFormatVisitor(JsonFormatVisitorWrapper visitor, JavaType typeHint)
            throws JsonMappingException {
        if (visitor != null) {
            visitor.expectStringFormat(typeHint);
        }
    }

}

and register it

module.addSerializer(IObjectWithId.class, new IObjectWithIdSerializer());

The result is

|processedUserIds
|Array[Object]
|true
|Some description that explains that it is an array of IDs.

and thus there are no nested fields or any recursion.

Note: To avoid recursive expansion @RestdocsNotExpanded can be used.

ChristianSch commented 5 years ago

Thank you for your time. This doesn't work for me as I need the models sometimes as objects, not only as their id's. With @RestdocsNotExpanded I have no problems besides the wrong type for the arrays. This works for me though. Thanks.

fbenz commented 5 years ago

Glad that you got it working. It's of course not perfect but there is also no easy solution in SARD.