Skip to content

Conversation

jeonsworld
Copy link

Summary

This PR adds a new append operation to python-json-patch for efficient text streaming in chat applications. When
streaming text character-by-character or word-by-word, the standard replace operation requires sending the entire string
with each update. The new append operation only sends the incremental text, significantly reducing payload size and
improving performance.

This method is used by official ChatGPT and is an implementation optimized for chat.

Key Features

  • New append operation: Appends text to existing string values
  • Optional RFC 6902 extension: Controlled via use_append_ops parameter
  • Optimized notation support: Reduces payload size for consecutive operations
  • Smart patch generation: make_patch() automatically detects append scenarios when enabled
  • Full backward compatibility: Maintains RFC 6902 compliance by default

Implementation Details

  1. AppendOperation class: New operation type that appends text to strings at specified JSON paths
    {"op": "append", "path": "/message/content", "value": "Hello"}

  2. Optional activation: The append operation is disabled by default for RFC 6902 compliance

# Standard RFC 6902 mode (default)
patch = jsonpatch.make_patch(src, dst)  # Uses 'replace' for string changes

# Extended mode with append operation
patch = jsonpatch.make_patch(src, dst, use_append_ops=True)  # Uses 'append' for string concatenation
  1. Optimized notation for streaming (when use_append_ops=True):
  • Standard: {"op": "append", "path": "/message", "value": "text"}
  • Short: {"p": "/message", "o": "append", "v": "text"}
  • Optimized: {"v": "text"} (reuses previous path/operation)
  1. Smart patch generation: When using make_patch() with use_append_ops=True, the library detects when a string change is an append operation:
src = {"message": "Hello"}
dst = {"message": "Hello World"}

# RFC 6902 standard mode (default)
patch = jsonpatch.make_patch(src, dst)
# Generates: [{"op": "replace", "path": "/message", "value": "Hello World"}]

# Extended mode with append ops
patch = jsonpatch.make_patch(src, dst, use_append_ops=True)
# Generates: [{"op": "append", "path": "/message", "value": " World"}]

API Changes

All functions now accept an optional use_append_ops parameter:

  • apply_patch(doc, patch, use_append_ops=False)
  • make_patch(src, dst, use_append_ops=False)
  • JsonPatch(patch, use_append_ops=False)
  • JsonPatch.from_string(patch_str, use_append_ops=False)
  • JsonPatch.from_diff(src, dst, use_append_ops=False)

Performance

The demo script shows append operations are ~2.8x faster than replace operations for streaming scenarios.

Backward Compatibility

  • Default behavior unchanged: Without the use_append_ops parameter, the library operates in strict RFC 6902 mode
  • Opt-in extension: Users must explicitly enable append operations
  • Error handling: Using append operation without enabling use_append_ops raises InvalidJsonPatch error

- Add new `append` operation as an optional extension to RFC 6902
- Introduce `use_append_ops` parameter to enable/disable append operations
- Support automatic detection of string append scenarios in make_patch()
- Maintain full backward compatibility with RFC 6902 by default
- Enable optimized notation for consecutive append operations in streaming

The append operation is disabled by default to maintain RFC 6902 compliance.
Users can opt-in by setting use_append_ops=True in apply_patch(), make_patch(), and JsonPatch methods.

This feature is particularly useful for chat applications and streaming scenarios where text is appended incrementally, reducing payload size by ~2.8x compared to replace operations.
@jeonsworld
Copy link
Author

@stefankoegl This PR is resubmitted with the append operation made optional for RFC 6902 compliance.

@stefankoegl stefankoegl requested a review from Copilot September 2, 2025 20:13
Copy link

@Copilot Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull Request Overview

This PR adds a new append operation to python-json-patch specifically optimized for text streaming in chat applications. When streaming text character-by-character or word-by-word, this reduces payload size by only sending incremental text instead of the entire string with each update.

Key changes include:

  • New AppendOperation class for string concatenation
  • Optional RFC 6902 extension controlled via use_append_ops parameter
  • Smart patch generation that automatically detects append scenarios when enabled

Tip: Customize your code reviews with copilot-instructions.md. Create the file or learn how to get started.

Comment on lines 1018 to +1032
else:
self._item_replaced(path, key, dst)
# Check if this is a string append operation (only if append ops are enabled)
if self.use_append_ops and isinstance(src, basestring) and isinstance(dst, basestring) and dst.startswith(
src):
appended_text = dst[len(src):]
if appended_text: # Only create append op if there's actual text to append
self.insert(AppendOperation({
'op': 'append',
'path': _path_join(path, key),
'value': appended_text,
}, pointer_cls=self.pointer_cls))
else:
self._item_replaced(path, key, dst)
else:
self._item_replaced(path, key, dst)
Copy link
Preview

Copilot AI Sep 2, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The logic flow is incorrect - there's an unnecessary else: block at line 1018 that creates unreachable code. The else: clause at line 1031 will never execute because it's nested inside the outer else: block, making the string append detection logic unreachable in most cases.

Copilot uses AI. Check for mistakes.

raise JsonPatchConflict("Cannot append to root document")

try:
if isinstance(subobj[part], basestring):
Copy link
Preview

Copilot AI Sep 2, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The append operation should validate that the 'value' parameter is a string before attempting concatenation. Currently, if 'value' is not a string, the += operation could succeed but produce unexpected results (e.g., concatenating a list to a string).

Suggested change
if isinstance(subobj[part], basestring):
if isinstance(subobj[part], basestring):
if not isinstance(self.operation['value'], basestring):
raise JsonPatchConflict("Cannot append non-string value to string")

Copilot uses AI. Check for mistakes.

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

Successfully merging this pull request may close these issues.

1 participant