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 <optional>
22#include <set>
23#include <string>
24#include <utility>
25
26#include "adapter_for.hpp"
27
28namespace jsonv
29{
30
31/// \addtogroup Serialization
32/// \{
33
34/// What to do when serializing a keyed subtype of a \c polymorphic_adapter. See
35/// \c polymorphic_adapter::add_subtype_keyed.
36enum class keyed_subtype_action : unsigned char
37{
38 /// Don't do any checking or insertion of the expected key/value pair.
39 none,
40 /// Ensure the correct key/value pair was inserted by serialization. Throws \c std::runtime_error if it wasn't.
41 check,
42 /// Insert the correct key/value pair as part of serialization. Throws \c std::runtime_error if the key is already
43 /// present.
44 insert
45};
46
47/// An adapter which can create polymorphic types. This allows you to parse JSON directly into a type heirarchy without
48/// some middle layer.
49///
50/// @code
51/// [
52/// {
53/// "type": "worker",
54/// "name": "Adam"
55/// },
56/// {
57/// "type": "manager",
58/// "name": "Bob",
59/// "head": "Development"
60/// }
61/// ]
62/// @endcode
63///
64/// Choosing a subtype means looking at the value before extracting it, which a \c reader -- a forward cursor -- cannot
65/// do by itself. Each discriminator is shown as little of the value as answers it, read without moving the reader, and
66/// the subtype it picks is then extracted from the reader directly:
67///
68/// - From a \c value, every discriminator is shown that value. Nothing is copied.
69/// - From JSON text, one registered with \c add_subtype_keyed is shown an object holding only the discriminating
70/// members, found by stepping over every other member whole. One registered with \c add_subtype can ask anything
71/// of the value, so the first time one of those has to be asked the subtree is materialised for it. Registering
72/// the keyed subtypes first keeps a matching document from ever being materialised.
73///
74/// \tparam TPointer Some pointer-like type (likely \c unique_ptr or \c shared_ptr) you wish to extract values into. It
75/// must support \c operator*, an explicit conversion to \c bool, construction with a pointer to a
76/// subtype of what it contains and default construction.
77///
78template <typename TPointer>
80 public adapter_for<TPointer>
81{
82public:
83 using match_predicate = std::function<bool (extraction_context&, const value&)>;
84
85public:
86 polymorphic_adapter() = default;
87
88 /// Add a subtype which can be transformed into \c TPointer which will be called if the discriminator \a pred is
89 /// matched.
90 ///
91 /// \see add_subtype_keyed
92 template <typename T>
93 void add_subtype(match_predicate pred)
94 {
95 emplace_subtype<T>(std::move(pred), false);
96 }
97
98 /// Add a subtype which can be transformed into \c TPointer which will be called if given a JSON \c value with
99 /// \c kind::object which has a member with \a key and the provided \a expected_value.
100 ///
101 /// \see add_subtype
102 template <typename T>
103 void add_subtype_keyed(std::string key,
104 value expected_value,
106 {
107 std::type_index tidx = std::type_index(typeid(T));
108 if (!_serialization_actions.emplace(tidx, std::make_tuple(key, expected_value, action)).second)
109 throw duplicate_type_error("polymorphic_adapter subtype", std::type_index(typeid(T)));
110
111 // Recorded before the subtype, so a failure to record it cannot leave a keyed subtype whose key is never read.
112 _discriminator_keys.insert(key);
113
114 match_predicate op = [key = std::move(key), expected_value = std::move(expected_value)]
116 {
117 if (!value.is_object())
118 return false;
119 auto iter = value.find(key);
120 return iter != value.end_object()
121 && iter->second == expected_value;
122 };
123 emplace_subtype<T>(std::move(op), true);
124 }
125
126 /// \{
127
128 /// When extracting a C++ value, should \c kind::null in JSON automatically become a default-constructed \c TPointer
129 /// (which is usually the \c null representation)?
130 void check_null_input(bool on)
131 {
132 _check_null_input = on;
133 }
134
136 bool check_null_input() const
137 {
138 return _check_null_input;
139 }
140 /// \}
141
142 /// \{
143
144 /// When converting with \c to_json, should a \c null input translate into a \c kind::null?
145 void check_null_output(bool on)
146 {
147 _check_null_output = on;
148 }
149
151 bool check_null_output() const
152 {
153 return _check_null_output;
154 }
155 /// \}
156
157protected:
159 virtual std::expected<TPointer, ast_node_type> create(extraction_context& context, reader& from) const override
160 {
161 // A value-backed reader renders a non-finite `kind::decimal` as `literal_null`, so where there is a `value` to
162 // ask, its `kind` decides -- the same rule `optional_adapter` follows for the same reason.
164 if (_check_null_input && (lent ? lent->kind() == jsonv::kind::null
165 : from.current_type() == ast_node_type::literal_null
166 )
167 )
168 {
169 // The cursor steps before the pointer is built, so a `TPointer` which refuses to default-construct fails
170 // with the value behind it.
171 try
172 {
173 (void) from.next_token();
174 return TPointer();
175 }
176 catch (...)
177 {
178 context.note_value_consumed(from);
179 throw;
180 }
181 }
182
183 // Each is read at most once, and only if some discriminator needs it. Neither moves `from`. Both settle a
184 // repeated key the way the subtype will when it reads the document, or the subtype chosen and the one built
185 // could disagree about what the discriminator said.
186 std::optional<value> members;
187 std::optional<value> whole;
188 auto subject = [&] (const subtype& sub) -> const value&
189 {
190 if (lent)
191 return *lent;
192
193 // A keyed discriminator reads one member, which the whole subtree has just as well as the
194 // projection does -- so once the whole has been paid for, there is no reason to scan again.
195 if (sub.keyed && !whole)
196 {
197 if (!members)
198 members.emplace(detail::peek_members(context, from, _discriminator_keys));
199 return *members;
200 }
201
202 if (!whole)
203 whole.emplace(detail::peek_value(context, from));
204 return *whole;
205 };
206
207 const subtype* chosen = nullptr;
208 {
209 // From text, a discriminator is shown a copy which dies with this call, so a view of anything in it would
210 // dangle. Saying so is what makes extracting one refuse, as it did when the bridge built that copy. From a
211 // `value` it is shown the caller's own tree, which a view may name.
212 std::optional<detail::temporary_source_scope> temporary;
213 if (!lent)
214 temporary.emplace(context);
215
216 for (const auto& sub : _subtypes)
217 {
218 if (sub.predicate(context, subject(sub)))
219 {
220 chosen = &sub;
221 break;
222 }
223 }
224 }
225
226 // Out of that scope, the chosen subtype reads the reader itself: a view it holds names the source rather than
227 // a temporary, and a position it takes from the reader is in the document rather than in a copy of it.
228 if (chosen)
229 return chosen->create(context, from);
230
231 // The cursor is still on the value, which is where a problem about it belongs and what whoever recovers from it
232 // expects to step over.
233 std::string message = "No discriminators matched JSON value";
234 try
235 {
236 const value& unmatched = lent ? *lent
237 : whole ? *whole
238 : whole.emplace(detail::peek_value(context, from));
239 message += ": " + to_string(unmatched);
240 }
241 catch (...) // NOLINT(bugprone-empty-catch): describing the failure must not replace it
242 { }
243 return context.problem(context.problem_path(from), std::move(message));
244 }
245
247 virtual value to_json(const serialization_context& context, const TPointer& from) const override
248 {
249 if (_check_null_output && !from)
250 return null;
251
252 value serialized = context.to_json(typeid(*from), static_cast<const void*>(&*from));
253
254 auto action_iter = _serialization_actions.find(std::type_index(typeid(*from)));
255 if (action_iter != _serialization_actions.end())
256 {
257 auto errmsg = [&]()
258 {
259 return " polymorphic_adapter<" + demangle(typeid(TPointer).name()) + ">"
260 "subtype(" + demangle(typeid(*from).name()) + ")";
261 };
262
263 const std::string& key = std::get<0>(action_iter->second);
264 const value& val = std::get<1>(action_iter->second);
265 const keyed_subtype_action& action = std::get<2>(action_iter->second);
266
267 switch (action)
268 {
270 break;
272 if (!serialized.is_object())
273 throw std::runtime_error("Expected keyed subtype to serialize as an object." + errmsg());
274 if (!serialized.count(key))
275 throw std::runtime_error("Expected subtype key not found." + errmsg());
276 if (serialized.at(key) != val)
277 throw std::runtime_error("Expected subtype key is not the expected value." + errmsg());
278 break;
280 if (!serialized.is_object())
281 throw std::runtime_error("Expected keyed subtype to serialize as an object." + errmsg());
282 if (serialized.count(key))
283 throw std::runtime_error("Subtype key already present when trying to insert." + errmsg());
284 serialized[key] = val;
285 break;
286 default:
287 throw std::runtime_error("Unknown keyed_subtype_action.");
288 }
289 }
290
291 return serialized;
292 }
293
294private:
295 using create_function = std::function<std::expected<TPointer, ast_node_type> (extraction_context&, reader&)>;
296
297 struct subtype
298 {
299 match_predicate predicate;
300 /// Does \ref predicate read nothing but a member named in \ref _discriminator_keys? If so it can be shown
301 /// \c detail::peek_members in place of the whole value.
302 bool keyed;
303 create_function create;
304 };
305
306 template <typename T>
307 void emplace_subtype(match_predicate pred, bool keyed)
308 {
309 _subtypes.push_back(subtype{ std::move(pred),
310 keyed,
311 [] (extraction_context& context, reader& from)
312 -> std::expected<TPointer, ast_node_type>
313 {
314 // A failure `extract` reports is returned, not thrown. What can throw is
315 // everything after the cursor has stepped past the value: `extract` moving
316 // the `T` it built out to here, the allocation, and the move into it -- all
317 // of which fail with that value behind the cursor.
318 try
319 {
320 auto extracted = context.extract<T>(from);
321 if (!extracted)
322 return std::unexpected(extracted.error());
323
324 return TPointer(new T(*std::move(extracted)));
325 }
326 catch (...)
327 {
328 context.note_value_consumed(from);
329 throw;
330 }
331 }
332 }
333 );
334 }
335
336private:
337 using serialization_action = std::tuple<std::string, value, keyed_subtype_action>;
338
339 std::vector<subtype> _subtypes;
340 std::set<std::string, std::less<>> _discriminator_keys;
341 std::map<std::type_index, serialization_action> _serialization_actions;
342 bool _check_null_input = false;
343 bool _check_null_output = false;
344};
345
346/// \}
347
348}
Copyright (c) 2015-2026 by Travis Gockel.
An adapter for the type T.
Provides extra information to routines used for extraction and serialization.
Definition context.hpp:26
Exception thrown if an insertion of an extractor or serializer into a formats is attempted,...
Definition formats.hpp:46
Provides extra information to routines used for extraction, collects the problems they encounter,...
Definition extract.hpp:391
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 ...
bool check_null_output() const
When converting with to_json, should a null input translate into a kind::null?
virtual std::expected< TPointer, ast_node_type > create(extraction_context &context, reader &from) const override
Create an instance of T by reading from.
bool check_null_input() const
When extracting a C++ value, should kind::null in JSON automatically become a default-constructed TPo...
void check_null_output(bool on)
When converting with to_json, should a null input translate into a kind::null?
void check_null_input(bool on)
When extracting a C++ value, should kind::null in JSON automatically become a default-constructed TPo...
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:104
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.
Represents a single JSON value, which can be any one of a potential kind, each behaving slightly diff...
Definition value.hpp:113
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.
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:132
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.
JSONV_PUBLIC const value null
An instance with kind::null.
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.