Search code examples
jacksonspring-dataspring-data-neo4jspring-projections

Spring open projections + jackson: JsonType annotation written even if a @Value throws an exception


The project

I am using open projections with an annotation that sets the type name. Impacts are a recursive interface that adds a property impacts to all objects that have impacts. I only copied relevant/cleaned up code to keep it clear.

    @GetMapping("/{id}/impacts")
    public ImpactOverview getImpacts(@PathVariable("id") String id) {
        return repository.findImpactByLabel(id, Impacts.class);
    }
public interface Impacts extends Field,ImpactOverview {
    // repo variable is simple spring data repository with a id and projection class params.
    @JsonIgnore
    @Value("#{@repo.findTable(target.label, T(com.company.view.projections.field.impacts.Table))}")
    Table getTable();

    default Collection<? extends ImpactOverview> impacts() {
        //combine is just making a big collection of collection fields & singular fields
        return combine(getTable(), ...);
    }

}
public interface Table extends com.company.view.projections.table.Table,ImpactOverview {}
@JsonTypeName(AssetType.FIELD_TYPENAME)
public interface Field {}
@JsonTypeName(AssetType.TABLE_TYPENAME)
public interface Table {}
@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "type")
public interface ImpactOverview {
    @JsonInclude(JsonInclude.Include.NON_EMPTY)
    @JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "type")
    default Collection<? extends ImpactOverview> impacts() {
        return null;
    }
}

Basically this works very well and results in a nested impact overview json structure as follows:

{
    "type": "field",
    ...
    "impacts: [
        {
            "type": "table",
            ...
        },
        {
            "type": "...",
            ...
            impacts: [...]
        }
    ]
}

Readable as follows: if something changes in this field, we have impact on this table, on this ...(eg. codebase, file...). If we impact eg. the codebase because the field changes, that change might have impact on this program. A recursive nested loop of impacts depending on predefined structure laid out by business.

We have also coded a ControllerAdvice that extends ResponseEntityExcepionHandler.

@ControllerAdvice
@Slf4j
@RequiredArgsConstructor
public class ControllerExceptionHandler extends ResponseEntityExceptionHandler {

    @Override
    protected ResponseEntity<Object> handleExceptionInternal(final Exception ex,
                                                             final Object body,
                                                             final HttpHeaders headers,
                                                             final HttpStatusCode statusCode,
                                                             final WebRequest request) {
        return toCompanyException(ex);
    }

    @ResponseBody
    ResponseEntity<Object> toCompanyException(@NonNull Exception e) {
        return ResponseEntity.status(Status.of(e)).body(Body.of(e));
    }
}

The problem

In case this repo.findTable call fails, a HttpMessageNotWritable exception is thrown by AbstractJackson2HttpMessageConverter, we are receiving a json structure that looks like this:

{
    "type": "FIELD"
}{
    "exception": "http-message-not-writable",
    "message": "exception"
}

This happens because the BeanSerializerBase::serializeWithType method does a writeTypePrefix in which the "type": "field" gets written. Only then the exception happens in the serializeFields call.

At that moment the field type is already written. How do I overcome this issue? We only want the exception object to be returned. If i call all getImpacts up front, all methods will be called/tested, but I will execute all queries twice? Of will there be caching in the proxy of the interface. Or should I store it in some Map<String, Object> kinda object and have that map be parsed by Jackson?


Solution

  • Found out this is caused by this issue I openend in spring framework

    Can be bypassed by removing the extends ResponseEntityExceptionHandler for us for now.