Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

JsonWriteContext could hold current value. #631

Closed
Antibrumm opened this issue Nov 25, 2014 · 7 comments
Closed

JsonWriteContext could hold current value. #631

Antibrumm opened this issue Nov 25, 2014 · 7 comments
Milestone

Comments

@Antibrumm
Copy link

It would be great if we could access the currentValue from the JsonWriteContext. It would give us more flexibility during filtering.

In my particular case I have implemented a BackReferenceAwareBeanSerializer to be able to break bi-directional relationships. As a side effect I'm also able to break circular references within the path.

For this to work the JsonWriteContext needs to know the currentValue.
Unfortunately the JsonWriteContext.writeValue method is final, as well as the UTF8JsonGenerators.writeStartObject/writeStartArray methods, so I was not able to simply call setCurrentValue directly from within these classes. I had to abuse a TypeSerializer as this one can act as a callback on any write operation.

Currently I had to override these classes and it would be great if that could be simplified:

  • JsonWriteContext (add currentValue attribute)
  • UTF8JsonGenerator (set _writeContext to new context)
  • MappingJsonFactory (override _createUTF8Generator and copy to use new generator)
  • AsPropertyTypeSerializer (override all writePrefix methods to set currentValue on context)
  • BeanSerializerBase (that my actual use-case)
  • ObjectMapper (call setDefaultTyping and setSerializerFactory)

During serialization the BackReferenceAwareBeanSerializer walks back up the context hierarchy and compares the pojo with the currentValues and writes the "level" of that as @backReference.

The ObjectIdGenerator feature would not be sufficient here as generated ids are "global". This implementation simply breaks references but continues serializing each path indepentantly.

@cowtowncoder
Copy link
Member

Interesting thought. I'll have to think about this for a bit, but this might work; I assume it would matter with structured types (i.e. things that come from JSON Object, Array).

One practical challenge may be that some datatypes use an alternative JsonStreamContext sub-classes; however, this would just mean that methods should be added there, to be overridden by sub-classes.

Another, possibly bigger challenge is that in some cases current value is known quite late: specifically, when using Creator methods, instance can only be created once all parameters for that are read.
However that is just a limitation.

Maybe I can play with this and verify there is no measurable performance penalty for added state, update calls.

@cowtowncoder
Copy link
Member

One additional comment: earlier I was thinking of storing the current POJO (and perhaps Collection/Map) being handled in DeserializationContext (or, more generally, DatabindContext).
But while that would work, doing this via JsonParser/JsonGenerator has two specific benefits:

  1. It handles nesting (i.e. allows access to full parent stack, not just latest one)
  2. It is available from streaming level

There is also one potential downside -- when constructing temporary readers (from buffered content), access may be compromise -- but that is probably something that can be solved relatively easily.

@cowtowncoder
Copy link
Member

Implemented streaming API support as per:

FasterXML/jackson-core#168

and so far no measurable negative performance impact found; so I think this can be added as suggested. I'll add it for both reading and writing, as it is likely to be of some use for both.

cowtowncoder added a commit that referenced this issue Nov 27, 2014
@Antibrumm
Copy link
Author

Nice work. Thanks.

I don't think i will have time this week to play with the snapshot and give you some feedback already, but i hope next week will be fine. (I'm just using the serializer part for now.)

@cowtowncoder
Copy link
Member

No prob: I need to add actual tests myself, to verify handling.

I think this is a great addition, thanks once again for suggesting it. I can now also mark another RFE obsolete for databind, since this should cover the intended use case.

@cowtowncoder
Copy link
Member

Ok, as per earlier notes, implemented both for JsonGenerator and JsonParser. Probably more useful for generator, but seemed logical to support for both. Information stored in parsing/output context, accessed via parser/generator for convenience.
And I tried to add proper set methods in standard serializers/deserializers; however, it is quite possible I missed some cases. If so, just let me know and I'll add calls (or merge Pull Request for the same).

@Antibrumm
Copy link
Author

Better late than never.. :)
I finally had time to use the new version and it looks pretty good. I was able to remove several of my needed classes 👍
As you expected there are some "setCurrentValue" calls still missing. As I already have a special BeanSerializer i was able to fix these inside the serializeFields and serializeFieldsFiltered methods.

I think the main missing points comes from the class BeanSerializerBase within these methods we should have to set the current values too:

  • serializeWithType
  • _serializeObjectId

I'm not fully sure but it might be easier to implement the setCurrentValue in the JsonGenerator? Basically each time a writeStartObject and writeStartArray methods is called?

As an addon here's my BackReferenceAwareBeanSerializer :)

public class BackReferenceAwareBeanSerializer extends BeanSerializerBase {

    private static final long serialVersionUID = 1L;

    public BackReferenceAwareBeanSerializer(final BeanSerializerBase src) {
        super(src);
    }

    protected BackReferenceAwareBeanSerializer(final BeanSerializerBase src, final ObjectIdWriter objectIdWriter) {
        super(src, objectIdWriter);
    }

    protected BackReferenceAwareBeanSerializer(final BeanSerializerBase src, final ObjectIdWriter objectIdWriter,
        final Object filterId) {
        super(src, objectIdWriter, filterId);
    }

    protected BackReferenceAwareBeanSerializer(final BeanSerializerBase src, final String[] toIgnore) {
        super(src, toIgnore);
    }

    @Override
    protected BeanSerializerBase asArraySerializer() {
        if ((_objectIdWriter == null) && (_anyGetterWriter == null) && (_propertyFilterId == null)) {
            return new BeanAsArraySerializer(this);
        }
        return this;
    }

    private boolean checkAndWriteAsBackReference(final Object bean, final JsonGenerator jgen) throws IOException {
        int backRef = getBackReferenceLevel(bean, jgen);
        if (backRef < 0) {
            jgen.writeFieldName("@backReference");
            jgen.writeNumber(backRef);
            return true;
        }
        return false;
    }

    private int getBackReferenceLevel(final Object pojo, final JsonGenerator jgen) {
        if (pojo == null) {
            return 0;
        }
        JsonStreamContext context = jgen.getOutputContext();
        JsonStreamContext parent = context.getParent();
        int lvl = -1;
        while (!parent.inRoot()) {
            if (parent.getCurrentValue().equals(pojo)) {
                return lvl;
            }
            parent = parent.getParent();
            --lvl;
        }
        return 0;
    }

    @Override
    public final void serialize(final Object bean, final JsonGenerator jgen, final SerializerProvider provider)
            throws IOException, JsonGenerationException {
        if (_objectIdWriter != null) {
            _serializeWithObjectId(bean, jgen, provider, true);
            return;
        }
        jgen.writeStartObject();
        if (_propertyFilterId != null) {
            serializeFieldsFiltered(bean, jgen, provider);
        } else {
            serializeFields(bean, jgen, provider);
        }
        jgen.writeEndObject();
    }

    @Override
    protected void serializeFields(final Object bean, final JsonGenerator jgen, final SerializerProvider provider)
            throws IOException, JsonGenerationException {
        jgen.setCurrentValue(bean);
        if (checkAndWriteAsBackReference(bean, jgen)) {
            return;
        }
        super.serializeFields(bean, jgen, provider);
    }

    @Override
    protected void serializeFieldsFiltered(final Object bean, final JsonGenerator jgen,
        final SerializerProvider provider) throws IOException, JsonGenerationException {
        jgen.setCurrentValue(bean);
        if (checkAndWriteAsBackReference(bean, jgen)) {
            return;
        }
        super.serializeFieldsFiltered(bean, jgen, provider);
    }

    @Override
    public String toString() {
        return "BeanSerializer for " + handledType().getName();
    }

    @Override
    public JsonSerializer<Object> unwrappingSerializer(final NameTransformer unwrapper) {
        return new UnwrappingBeanSerializer(this, unwrapper);
    }

    @Override
    protected BeanSerializerBase withFilterId(final Object filterId) {
        return new BackReferenceAwareBeanSerializer(this, _objectIdWriter, filterId);
    }

    @Override
    protected BeanSerializerBase withIgnorals(final String[] toIgnore) {
        return new BackReferenceAwareBeanSerializer(this, toIgnore);
    }

    @Override
    public BeanSerializerBase withObjectIdWriter(final ObjectIdWriter objectIdWriter) {
        return new BackReferenceAwareBeanSerializer(this, objectIdWriter, _propertyFilterId);
    }
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants