Skip to content

Commit 45d4552

Browse files
committed
Add documentation for StructuralContext to README
1 parent 8f53dc0 commit 45d4552

File tree

1 file changed

+164
-0
lines changed

1 file changed

+164
-0
lines changed

README.md

Lines changed: 164 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,3 +106,167 @@ JSON.lower(p::Point2D) = [p.x, p.y]
106106

107107
Define a custom serialization rule for a particular data type. Must return a
108108
value that can be directly serialized; see help for more details.
109+
110+
### Customizing JSON
111+
112+
Users may find the default behaviour of JSON inappropriate for their use case. In
113+
such cases, JSON provides two mechanisms for users to customize serialization. The
114+
first method, `JSON.Writer.StructuralContext`, is used to customize the cosmetic
115+
properties of the serialized JSON. (For example, the default pretty printing vs.
116+
compact printing is supported by provided two different `StructuralContext`s.)
117+
Examples of applications for which `StructuralContext` is appropriate include:
118+
particular formatting demands for JSON (maybe not in compliance with the JSON
119+
standard) or JSON-like formats with different syntax.
120+
121+
The second method, `JSON.Serializations.Serialization`, is used to control the
122+
translation of Julia objects into JSON serialization instructions. In most cases,
123+
writing a method for `JSON.lower` (as mentioned above) is sufficient to define
124+
JSON serializations for user-defined objects. However, this is not appropriate for
125+
overriding or deleting predefined serializations (since that would globally affect
126+
users of the `JSON` module and is an instance of dangerous
127+
[type piracy](https://docs.julialang.org/en/v1/manual/style-guide/index.html#Avoid-type-piracy-1)).
128+
For these use-cases, users should define a custom instance of `Serialization`.
129+
An example of an application for this use case includes: a commonly requested
130+
extension to JSON which serializes float NaN and infinite values as `NaN` or `Inf`,
131+
in contravention of the JSON standard.
132+
133+
Both methods are controlled by the `JSON.show_json` function, which has the following
134+
signature:
135+
136+
```
137+
JSON.show_json(io::StructuralContext, serialization::Serialization, object)
138+
```
139+
140+
which is expected to write to `io` in a way appropriate based on the rules of
141+
`Serialization`, but here `io` is usually (but not required to be) handled in a
142+
higher-level manner than a raw `IO` object would ordinarily be.
143+
144+
#### StructuralContext
145+
146+
To define a new `StructuralContext`, the following boilerplate is recommended:
147+
148+
```julia
149+
import JSON.Writer.StructuralContext
150+
[mutable] struct MyContext <: StructuralContext
151+
io::IO
152+
[ ... additional state / settings for context goes here ... ]
153+
end
154+
```
155+
156+
If your structural context is going to be very similar to the existing JSON
157+
contexts, it is also possible to instead subtype the abstract subtype
158+
`JSONContext` of `StructuralContext`. If this is the case, an `io::IO` field (as
159+
above) is preferred, although the default implementation will only use this
160+
for `write`, so replacing that method is enough to avoid this requirement.
161+
The default implementations will also use an internal
162+
163+
The following methods should be defined for your context, regardless of whether it
164+
subtypes `JSONContext` or `StructuralContext` directly. If some of these methods
165+
are omitted, then `CommonSerialization` cannot be generally used with this context.
166+
167+
```
168+
# called when the next object in a vector or next pair of a dict is to be written
169+
# (requiring a newline and indent for some contexts)
170+
# can do nothing if the context need not support indenting
171+
JSON.Writer.indent(io::MyContext)
172+
173+
# called for vectors/dicts to separate items, usually writes ","
174+
# unless this is the first element in a JSON array
175+
# (default implementation for JSONContext exists, but requires a mutable bool
176+
# `first` field, and this is an implementation detail not to be relied on;
177+
# to define own or delegate explicitly)
178+
JSON.Writer.delimit(io::MyContext)
179+
180+
# called for dicts to separate key and value, usually writes ": "
181+
JSON.Writer.separate(io::MyContext)
182+
183+
# called to indicate start and end of a vector
184+
JSON.Writer.begin_array(io::MyContext)
185+
JSON.Writer.end_array(io::MyContext)
186+
187+
# called to indicate start and end of a dict
188+
JSON.Writer.begin_object(io::MyContext)
189+
JSON.Writer.end_object(io::MyContext)
190+
```
191+
192+
For the following methods, `JSONContext` provides a default implementation,
193+
but it can be specialized. For `StructuralContext`s which are not
194+
`JSONContext`s, the `JSONContext` defaults are not appropriate and so are
195+
not available.
196+
197+
```julia
198+
# directly write a specific byte (if supported)
199+
# default implementation writes to underlying `.io` field
200+
# note that this enables JSONContext to act as any `io::IO`,
201+
# i.e. one can use `print`, `show`, etc.
202+
Base.write(io::MyContext, byte::UInt8)
203+
204+
# write "null"
205+
# default implementation writes to underlying `.io` field
206+
JSON.Writer.show_null(io::MyContext)
207+
208+
# write an object or string in a manner safe for JSON string
209+
# default implementation calls `print` but escapes each byte as appropriate
210+
# and adds double quotes around the content of `elt`
211+
JSON.Writer.show_string(io::MyContext, elt)
212+
213+
# write a new element of JSON array
214+
# default implementation calls delimit, then indent, then show_json
215+
JSON.Writer.show_element(io::MyContext, elt)
216+
217+
# write a key for a JSON object
218+
# default implementation calls delimit, then indent, then show_string,
219+
# then seperate
220+
JSON.Writer.show_key(io::MyContext, elt)
221+
222+
# write a key-value pair for a JSON object
223+
# default implementation calls show key, then show_json
224+
JSON.Writer.show_pair(io::MyContext, s::Serialization, key, value)
225+
```
226+
227+
What follows is an example of a `JSONContext` subtype which is very similar
228+
to the default context, but which uses `None` instead of `null` for JSON nulls,
229+
which is then generally compatible with Python object literal notation (PYON). It
230+
wraps a default `JSONContext` to delegate all the required methods to. Since
231+
the wrapped context already has a `.io`, this object does not need to include
232+
an `.io` field, and so the `write` method must also be delegated, since the default
233+
is not appropriate. The only other specialization needed is `show_null`.
234+
235+
```julia
236+
import JSON.Writer
237+
import JSON.Writer.JSONContext
238+
mutable struct PYONContext <: JSONContext
239+
underlying::JSONContext
240+
end
241+
242+
for delegate in [:indent,
243+
:delimit,
244+
:separate,
245+
:begin_array,
246+
:end_array,
247+
:begin_object,
248+
:end_object]
249+
@eval JSON.Writer.$delegate(io::PYONContext) = JSON.Writer.$delegate(io.underlying)
250+
end
251+
Base.write(io::PYONContext, byte::UInt8) = write(io.underlying, byte)
252+
253+
JSON.Writer.show_null(io::PYONContext) = print(io, "None")
254+
pyonprint(io::IO, obj) = let io = PYONContext(JSON.Writer.PrettyContext(io, 4))
255+
JSON.print(io, obj)
256+
return
257+
end
258+
```
259+
260+
The usage of this `pyonprint` function is as any other `print` function, e.g.
261+
262+
```julia
263+
julia> pyonprint(stdout, [1, 2, nothing])
264+
[
265+
1,
266+
2,
267+
None
268+
]
269+
270+
julia> sprint(pyonprint, missing)
271+
"None"
272+
```

0 commit comments

Comments
 (0)