JSON Voorhees
Killer JSON for C++
Loading...
Searching...
No Matches
polymorphic_adapter.hpp
Go to the documentation of this file.
1/// \file jsonv/serialization/polymorphic_adapter.hpp
2///
3/// Copyright (c) 2017-2026 by Travis Gockel. All rights reserved.
4///
5/// This program is free software: you can redistribute it and/or modify it under the terms of the Apache License
6/// as published by the Apache Software Foundation, either version 2 of the License, or (at your option) any later
7/// version.
8///
9/// \author Travis Gockel (travis@gockelhut.com)
10#pragma once
11
12#include <jsonv/config.hpp>
13#include <jsonv/demangle.hpp>
14#include <jsonv/kind.hpp>
15#include <jsonv/optional.hpp>
16#include <jsonv/reader.hpp>
18
19#include <expected>
20#include <functional>
21#include <map>
22#include <optional>
23#include <set>
24#include <string>
25#include <utility>
26
27#include "adapter_for.hpp"
28
29namespace jsonv
30{
31
32/// \addtogroup Serialization
33/// \{
34
35/// What to do when serializing a keyed subtype of a \c polymorphic_adapter. See
36/// \c polymorphic_adapter::add_subtype_keyed. Both \c check and \c insert build the subtype as a \c value before
37/// writing it, which \c polymorphic_adapter describes.
38enum class keyed_subtype_action : unsigned char
39{
40 /// Don't do any checking or insertion of the expected key/value pair.
41 none,
42 /// Ensure the correct key/value pair was inserted by serialization. Throws \c std::runtime_error if it wasn't.
43 check,
44 /// Insert the correct key/value pair as part of serialization. Throws \c std::runtime_error if the key is already
45 /// present.
46 insert
47};
48
49/// An adapter which can create polymorphic types. This allows you to parse JSON directly into a type heirarchy without
50/// some middle layer.
51///
52/// @code
53/// [
54/// {
55/// "type": "worker",
56/// "name": "Adam"
57/// },
58/// {
59/// "type": "manager",
60/// "name": "Bob",
61/// "head": "Development"
62/// }
63/// ]
64/// @endcode
65///
66/// Choosing a subtype means looking at the value before deserializing it, which a \c reader -- a forward cursor --
67/// cannot do by itself. Each discriminator is shown as little of the value as answers it, read without moving the
68/// reader, and the subtype it picks is then deserialized from the reader directly:
69///
70/// - From a \c value, every discriminator is shown that value. Nothing is copied.
71/// - From JSON text, one registered with \c add_subtype_keyed is shown an object holding only the discriminating
72/// members, found by stepping over every other member whole. One registered with \c add_subtype can ask anything
73/// of the value, so the first time one of those has to be asked the subtree is materialised for it. Registering
74/// the keyed subtypes first keeps a matching document from ever being materialised.
75///
76/// Serializing writes the pointed-to object where the \c writer is, through the serializer registered for its dynamic
77/// type. A subtype registered with \c keyed_subtype_action::check or \c keyed_subtype_action::insert is the exception,
78/// since the action works on the finished object: it is built as a \c value through \c serialization_context::to_json,
79/// checked or completed, and only then written whole. Its members therefore come out sorted by key, as they do from
80/// \c to_json, and a \c serialization_error raised inside it reports a path from its own root rather than the
81/// document's. One the action refuses has written nothing.
82///
83/// \tparam TPointer Some pointer-like type (likely \c unique_ptr or \c shared_ptr) you wish to deserialize values into.
84/// It must support \c operator*, an explicit conversion to \c bool, construction with a pointer to a
85/// subtype of what it contains and default construction.
86///
87template <typename TPointer>
89 public adapter_for<TPointer>
90{
91public:
92 using match_predicate = std::function<bool (deserialization_context&, const value&)>;
93
94public:
95 polymorphic_adapter() = default;
96
97 /// Add a subtype which can be transformed into \c TPointer which will be called if the discriminator \a pred is
98 /// matched.
99 ///
100 /// \see add_subtype_keyed
101 template <typename T>
102 void add_subtype(match_predicate pred)
103 {
104 emplace_subtype<T>(std::move(pred), false);
105 }
106
107 /// Add a subtype which can be transformed into \c TPointer which will be called if given a JSON \c value with
108 /// \c kind::object which has a member with \a key and the provided \a expected_value.
109 ///
110 /// \see add_subtype
111 template <typename T>
112 void add_subtype_keyed(std::string key,
113 value expected_value,
115 {
116 std::type_index tidx = std::type_index(typeid(T));
117 if (!_serialization_actions.emplace(tidx, std::make_tuple(key, expected_value, action)).second)
118 throw duplicate_type_error("polymorphic_adapter subtype", std::type_index(typeid(T)));
119
120 // Recorded before the subtype, so a failure to record it cannot leave a keyed subtype whose key is never read.
121 _discriminator_keys.insert(key);
122
123 match_predicate op = [key = std::move(key), expected_value = std::move(expected_value)]
125 {
126 if (!value.is_object())
127 return false;
128 auto iter = value.find(key);
129 return iter != value.end_object()
130 && iter->second == expected_value;
131 };
132 emplace_subtype<T>(std::move(op), true);
133 }
134
135 /// \{
136
137 /// When deserializing a C++ value, should \c kind::null in JSON automatically become a default-constructed \c
138 /// TPointer (which is usually the \c null representation)?
139 void check_null_input(bool on)
140 {
141 _check_null_input = on;
142 }
143
145 bool check_null_input() const
146 {
147 return _check_null_input;
148 }
149 /// \}
150
151 /// \{
152
153 /// When serializing, should an empty \c TPointer be written as \c null?
154 void check_null_output(bool on)
155 {
156 _check_null_output = on;
157 }
158
160 bool check_null_output() const
161 {
162 return _check_null_output;
163 }
164 /// \}
165
166protected:
168 virtual std::expected<TPointer, ast_node_type> create(deserialization_context& context, reader& from) const override
169 {
170 // A value-backed reader renders a non-finite `kind::decimal` as `literal_null`, so where there is a `value` to
171 // ask, its `kind` decides -- the same rule `optional_adapter` follows for the same reason.
173 if (_check_null_input && (lent ? lent->kind() == jsonv::kind::null
174 : from.current_type() == ast_node_type::literal_null
175 )
176 )
177 {
178 // The cursor steps before the pointer is built, so a `TPointer` which refuses to default-construct fails
179 // with the value behind it.
180 try
181 {
182 (void) from.next_token();
183 return TPointer();
184 }
185 catch (...)
186 {
187 context.note_value_consumed(from);
188 throw;
189 }
190 }
191
192 // Each is read at most once, and only if some discriminator needs it. Neither moves `from`. Both settle a
193 // repeated key the way the subtype will when it reads the document, or the subtype chosen and the one built
194 // could disagree about what the discriminator said.
195 std::optional<value> members;
196 std::optional<value> whole;
197 auto subject = [&] (const subtype& sub) -> const value&
198 {
199 if (lent)
200 return *lent;
201
202 // A keyed discriminator reads one member, which the whole subtree has just as well as the
203 // projection does -- so once the whole has been paid for, there is no reason to scan again.
204 if (sub.keyed && !whole)
205 {
206 if (!members)
207 members.emplace(detail::peek_members(context, from, _discriminator_keys));
208 return *members;
209 }
210
211 if (!whole)
212 whole.emplace(detail::peek_value(context, from));
213 return *whole;
214 };
215
216 const subtype* chosen = nullptr;
217 {
218 // From text, a discriminator is shown a copy which dies with this call, so a view of anything in it would
219 // dangle. Saying so is what makes deserializing one refuse, as it did when the bridge built that copy. From
220 // a `value` it is shown the caller's own tree, which a view may name.
221 std::optional<detail::temporary_source_scope> temporary;
222 if (!lent)
223 temporary.emplace(context);
224
225 for (const auto& sub : _subtypes)
226 {
227 if (sub.predicate(context, subject(sub)))
228 {
229 chosen = &sub;
230 break;
231 }
232 }
233 }
234
235 // Out of that scope, the chosen subtype reads the reader itself: a view it holds names the source rather than
236 // a temporary, and a position it takes from the reader is in the document rather than in a copy of it.
237 if (chosen)
238 return chosen->create(context, from);
239
240 // The cursor is still on the value, which is where a problem about it belongs and what whoever recovers from it
241 // expects to step over.
242 std::string message = "No discriminators matched JSON value";
243 try
244 {
245 const value& unmatched = lent ? *lent
246 : whole ? *whole
247 : whole.emplace(detail::peek_value(context, from));
248 message += ": " + to_string(unmatched);
249 }
250 catch (...) // NOLINT(bugprone-empty-catch): describing the failure must not replace it
251 { }
252 return context.problem(context.problem_path(from), std::move(message));
253 }
254
255 /// Write the object \a from points to where \a to is, or \c null for an empty pointer under \c check_null_output. A
256 /// subtype registered with \c keyed_subtype_action::check or \c keyed_subtype_action::insert is built as a \c value
257 /// and written whole, as the class description says.
258 virtual void serialize(const serialization_context& context, const TPointer& from, writer& to) const override
259 {
260 if (_check_null_output && !from)
261 {
262 to.null();
263 return;
264 }
265
266 auto action_iter = _serialization_actions.find(std::type_index(typeid(*from)));
267 if (action_iter == _serialization_actions.end()
268 || std::get<2>(action_iter->second) == keyed_subtype_action::none
269 )
270 {
271 context.serialize(typeid(*from), static_cast<const void*>(&*from), to);
272 }
273 else
274 {
275 // Built before anything is written, so a subtype the action refuses leaves nothing of itself in the writer.
276 to.write(keyed_to_json(context, from, action_iter->second));
277 }
278 }
279
280private:
281 using serialization_action = std::tuple<std::string, value, keyed_subtype_action>;
282
283 /// Serialize \a from, a subtype registered with \c keyed_subtype_action::check or \c keyed_subtype_action::insert,
284 /// as a \c value and carry out its \a action on the finished object.
286 value keyed_to_json(const serialization_context& context,
287 const TPointer& from,
288 const serialization_action& action_entry
289 ) const
290 {
291 value serialized = context.to_json(typeid(*from), static_cast<const void*>(&*from));
292
293 auto errmsg = [&]()
294 {
295 return " polymorphic_adapter<" + demangle(typeid(TPointer).name()) + ">"
296 "subtype(" + demangle(typeid(*from).name()) + ")";
297 };
298
299 const std::string& key = std::get<0>(action_entry);
300 const value& val = std::get<1>(action_entry);
301 const keyed_subtype_action& action = std::get<2>(action_entry);
302
303 switch (action)
304 {
306 if (!serialized.is_object())
307 throw std::runtime_error("Expected keyed subtype to serialize as an object." + errmsg());
308 if (!serialized.count(key))
309 throw std::runtime_error("Expected subtype key not found." + errmsg());
310 if (serialized.at(key) != val)
311 throw std::runtime_error("Expected subtype key is not the expected value." + errmsg());
312 break;
314 if (!serialized.is_object())
315 throw std::runtime_error("Expected keyed subtype to serialize as an object." + errmsg());
316 if (serialized.count(key))
317 throw std::runtime_error("Subtype key already present when trying to insert." + errmsg());
318 serialized[key] = val;
319 break;
320 default:
321 throw std::runtime_error("Unknown keyed_subtype_action.");
322 }
323
324 return serialized;
325 }
326
327 using create_function = std::function<std::expected<TPointer, ast_node_type> (deserialization_context&, reader&)>;
328
329 struct subtype
330 {
331 match_predicate predicate;
332 /// Does \ref predicate read nothing but a member named in \ref _discriminator_keys? If so it can be shown
333 /// \c detail::peek_members in place of the whole value.
334 bool keyed;
335 create_function create;
336 };
337
338 template <typename T>
339 void emplace_subtype(match_predicate pred, bool keyed)
340 {
341 _subtypes.push_back(subtype{ std::move(pred),
342 keyed,
343 [] (deserialization_context& context, reader& from)
344 -> std::expected<TPointer, ast_node_type>
345 {
346 // A failure `deserialize` reports is returned, not thrown. What can throw is
347 // everything after the cursor has stepped past the value: `deserialize` moving
348 // the `T` it built out to here, the allocation, and the move into it -- all
349 // of which fail with that value behind the cursor.
350 try
351 {
352 auto deserialized = context.deserialize<T>(from);
353 if (!deserialized)
354 return std::unexpected(deserialized.error());
355
356 return TPointer(new T(*std::move(deserialized)));
357 }
358 catch (...)
359 {
360 context.note_value_consumed(from);
361 throw;
362 }
363 }
364 }
365 );
366 }
367
368private:
369 std::vector<subtype> _subtypes;
370 std::set<std::string, std::less<>> _discriminator_keys;
371 std::map<std::type_index, serialization_action> _serialization_actions;
372 bool _check_null_input = false;
373 bool _check_null_output = false;
374};
375
376/// \}
377
378}
Copyright (c) 2015-2026 by Travis Gockel.
An adapter for the type T.
Provides extra information to routines used for deserialization and serialization.
Definition context.hpp:26
Provides extra information to routines used for deserialization, collects the problems they encounter...
Exception thrown if an insertion of a deserializer or serializer into a formats is attempted,...
Definition formats.hpp:48
An adapter which can create polymorphic types.
void add_subtype_keyed(std::string key, value expected_value, keyed_subtype_action action=keyed_subtype_action::none)
Add a subtype which can be transformed into TPointer which will be called if given a JSON value with ...
virtual void serialize(const serialization_context &context, const TPointer &from, writer &to) const override
Write the object from points to where to is, or null for an empty pointer under check_null_output.
bool check_null_output() const
When serializing, should an empty TPointer be written as null?
virtual std::expected< TPointer, ast_node_type > create(deserialization_context &context, reader &from) const override
Create an instance of T by reading from.
bool check_null_input() const
When deserializing a C++ value, should kind::null in JSON automatically become a default-constructed ...
void check_null_output(bool on)
When serializing, should an empty TPointer be written as null?
void check_null_input(bool on)
When deserializing a C++ value, should kind::null in JSON automatically become a default-constructed ...
void add_subtype(match_predicate pred)
Add a subtype which can be transformed into TPointer which will be called if the discriminator pred i...
A reader instance reads from some form of JSON source (probably a string) and converts it into a JSON...
Definition reader.hpp:106
ast_node_type current_type() const
Get the type of the current AST node, which is always current().type().
bool next_token() noexcept
Go to the next token.
optional< const value & > current_value() const noexcept
Get the in-memory value this reader is positioned on, if it has one to lend.
Provides extra information to routines used for serialization: the formats to find other serializers ...
Definition serialize.hpp:42
Represents a single JSON value, which can be any one of a potential kind, each behaving slightly diff...
Definition value.hpp:156
size_type count(const std::string &key) const
Check if the given key exists in this object.
object_iterator end_object()
Get an iterator to the one past the end of this object.
object_iterator find(const std::string &key)
Attempt to locate a key-value pair with the provided key in this object.
value & at(size_type idx)
Get the value in this array at the given idx.
std::pair< object_iterator, bool > emplace(std::string key, value val)
Construct an element from key and val and insert it into this object.
bool is_object() const
Tests if this kind is kind::object.
A writer instance writes a JSON ast_node sequence to some form of sink: an encoder,...
Definition writer.hpp:68
writer & write(const value &source)
Write the whole of source as one value, as encoder::encode does.
writer & null()
Write a scalar value.
Copyright (c) 2014-2020 by Travis Gockel.
Copyright (c) 2015 by Travis Gockel.
#define JSONV_NODISCARD
Warn if the caller discards the result of this function.
Definition config.hpp:143
JSONV_PUBLIC std::string demangle(std::string_view source)
Convert the input source from a mangled type into a human-friendly version.
keyed_subtype_action
What to do when serializing a keyed subtype of a polymorphic_adapter.
@ check
Ensure the correct key/value pair was inserted by serialization. Throws std::runtime_error if it wasn...
@ none
Don't do any checking or insertion of the expected key/value pair.
@ insert
Insert the correct key/value pair as part of serialization.
JSONV_PUBLIC std::string to_string(const ast_node_type &type)
Get what operator<< writes for type as a std::string.
Copyright (c) 2019-2020 by Travis Gockel.
Pulls in an implementation of optional.
typename detail::optional_type< T >::type optional
Represents a value that may or may not be present.
Definition optional.hpp:239
Read a JSON AST.
Conversion between C++ types and JSON values.