diff --git a/doc/traits.html b/doc/traits.html
index b00765dc4..474b7f1a3 100644
--- a/doc/traits.html
+++ b/doc/traits.html
@@ -145,6 +145,22 @@
BOOST_CLASS_IMPLEMENTATION(my_class, boost::serialization::object_class_info)
+Take care when assigning primitive_type.
+It states that the archive already handles the type itself; it is not a request
+to make it do so. Archives implement primitives for the fundamental types and
+for std::string and
+std::wstring. For any other type they
+fall back to stream extraction, in the text and xml archives, or to a copy of
+sizeof(T) raw bytes, in the binary
+ones. Assigning the level to a type an archive knows nothing about therefore
+compiles, but reads back a wrong value rather than failing: declared this way,
+a std::pmr::string stops at the first
+space. A string whose allocator is not
+std::allocator needs no such
+declaration, as
+string.hpp
+serializes it just like std::string.
+
If implementation level is not explicitly assigned, the system uses
a default according to the following rules.
diff --git a/include/boost/serialization/string.hpp b/include/boost/serialization/string.hpp
index 54f1bcbb7..8c703f110 100644
--- a/include/boost/serialization/string.hpp
+++ b/include/boost/serialization/string.hpp
@@ -11,20 +11,97 @@
// serialization for stl string templates
// (C) Copyright 2002 Robert Ramey - http://www.rrsd.com .
+// Copyright 2026 Gennaro Prota.
// Distributed under the Boost Software License, Version 1.0.
// (See accompanying file LICENSE_1_0.txt or copy at
// http://www.boost.org/LICENSE_1_0.txt)
// See http://www.boost.org for updates, documentation, and revision history.
+#include
#include
#include
+#include
+#include
+#include
+#include
+
#include
+#include
+#include
BOOST_CLASS_IMPLEMENTATION(std::string, boost::serialization::primitive_type)
#ifndef BOOST_NO_STD_WSTRING
BOOST_CLASS_IMPLEMENTATION(std::wstring, boost::serialization::primitive_type)
#endif
+namespace boost {
+namespace serialization {
+
+/////////1/////////2/////////3/////////4/////////5/////////6/////////7/////////8
+// basic_string with an allocator of its own, std::pmr::string being the
+// usual case. The archives implement primitives for std::string and
+// std::wstring only, so such a string borrows the primitive of the
+// correspondingly built default allocated string. Passing a null name to
+// make_nvp keeps the representation identical to that of std::string in
+// every archive, xml included, so the two interoperate.
+
+template
+inline typename boost::disable_if<
+ boost::is_same >
+>::type
+save(
+ Archive & ar,
+ const std::basic_string & t,
+ const unsigned int /* file_version */
+){
+ const std::basic_string s(t.begin(), t.end());
+ ar << boost::serialization::make_nvp(NULL, s);
+}
+
+template
+inline typename boost::disable_if<
+ boost::is_same >
+>::type
+load(
+ Archive & ar,
+ std::basic_string & t,
+ const unsigned int /* file_version */
+){
+ std::basic_string s;
+ ar >> boost::serialization::make_nvp(NULL, s);
+ t.assign(s.begin(), s.end());
+}
+
+// split non-intrusive serialization function template into separate
+// non intrusive save/load function templates
+template
+inline typename boost::disable_if<
+ boost::is_same >
+>::type
+serialize(
+ Archive & ar,
+ std::basic_string & t,
+ const unsigned int file_version
+){
+ boost::serialization::split_free(ar, t, file_version);
+}
+
+// A string carries no class information and no version. This has to
+// specialize the same template BOOST_CLASS_IMPLEMENTATION does, so that the
+// full specializations naming std::string and std::wstring win over it.
+template
+struct implementation_level_impl<
+ const std::basic_string
+>
+{
+ typedef mpl::integral_c_tag tag;
+ typedef mpl::int_ type;
+ BOOST_STATIC_CONSTANT(int, value = object_serializable);
+};
+
+} // namespace serialization
+} // namespace boost
+
#endif // BOOST_SERIALIZATION_STRING_HPP
diff --git a/test/Jamfile.v2 b/test/Jamfile.v2
index ee765893c..d859054e8 100644
--- a/test/Jamfile.v2
+++ b/test/Jamfile.v2
@@ -110,6 +110,7 @@ test-suite "serialization" :
[ test-bsl-run_files test_slist_ptrs : A : : [ requires slist ] ] # BOOST_HAS_SLIST ] ]
[ test-bsl-run_files test_split ]
[ test-bsl-run_files test_stack : A ]
+ [ test-bsl-run_files test_string_allocator ]
[ test-bsl-run_files test_tracking ]
[ test-bsl-run_files test_unregistered ]
[ test-bsl-run_files test_unique_ptr ]
diff --git a/test/test_string_allocator.cpp b/test/test_string_allocator.cpp
new file mode 100644
index 000000000..c72e67ef5
--- /dev/null
+++ b/test/test_string_allocator.cpp
@@ -0,0 +1,177 @@
+/////////1/////////2/////////3/////////4/////////5/////////6/////////7/////////8
+// test_string_allocator.cpp
+
+// Copyright 2026 Gennaro Prota.
+// Distributed under the Boost Software License, Version 1.0.
+// (See accompanying file LICENSE_1_0.txt or copy at
+// http://www.boost.org/LICENSE_1_0.txt)
+
+// See http://www.boost.org for updates, documentation, and revision history.
+
+// A basic_string which does not use std::allocator, std::pmr::string being
+// the usual case, has to round trip like std::string does and to produce
+// the same representation, so that the two interoperate.
+
+// Reported by schorsch1976 in
+// https://github.com/boostorg/serialization/issues/267. The report showed
+// that declaring such a string primitive_type silently reads back only up
+// to the first space, which is what led to the support added here. Thanks
+// for the clear reproducer!
+
+#include
+#include
+#include
+#include
+#include
+
+#include
+#if defined(BOOST_NO_STDC_NAMESPACE)
+namespace std{
+ using ::remove;
+}
+#endif
+
+#include
+#include
+
+#include "test_tools.hpp"
+
+// The smallest allocator basic_string accepts, so that the string below
+// differs from std::string in its allocator only.
+template
+struct custom_allocator
+{
+ typedef T value_type;
+ custom_allocator(){}
+ template
+ custom_allocator(const custom_allocator &){}
+ T * allocate(std::size_t n){
+ return std::allocator().allocate(n);
+ }
+ void deallocate(T * p, std::size_t n){
+ std::allocator().deallocate(p, n);
+ }
+ template
+ bool operator==(const custom_allocator &) const {
+ return true;
+ }
+ template
+ bool operator!=(const custom_allocator &) const {
+ return false;
+ }
+};
+
+typedef std::basic_string<
+ char, std::char_traits, custom_allocator
+> custom_string;
+
+#ifndef BOOST_NO_STD_WSTRING
+typedef std::basic_string<
+ wchar_t, std::char_traits, custom_allocator
+> custom_wstring;
+#endif
+
+template
+struct data
+{
+ friend class boost::serialization::access;
+ template
+ void serialize(Archive & ar, const unsigned int /* file_version */){
+ ar & BOOST_SERIALIZATION_NVP(value);
+ ar & BOOST_SERIALIZATION_NVP(tail);
+ }
+ String value;
+ int tail;
+};
+
+template
+static void save(const char * testfile, const char * text){
+ data d;
+ d.value = text;
+ d.tail = 42;
+ test_ostream os(testfile, TEST_STREAM_FLAGS);
+ test_oarchive oa(os, TEST_ARCHIVE_FLAGS);
+ oa << BOOST_SERIALIZATION_NVP(d);
+}
+
+template
+static std::string load(const char * testfile){
+ data d;
+ d.tail = 0;
+ test_istream is(testfile, TEST_STREAM_FLAGS);
+ test_iarchive ia(is, TEST_ARCHIVE_FLAGS);
+ ia >> BOOST_SERIALIZATION_NVP(d);
+ // the member after the string has to survive too: reading a string
+ // past its end used to leave the rest of the archive unusable
+ BOOST_CHECK(42 == d.tail);
+ return std::string(d.value.begin(), d.value.end());
+}
+
+// text which the old primitive_type route mishandled: a space stopped the
+// token read, and the xml tag characters confused it further
+static const char * const texts[] = {
+ "D1",
+ "a b",
+ "",
+ " lead and trail ",
+ "tag and & < > chars"
+};
+
+static void test_roundtrip(const char * testfile, const char * text){
+ save(testfile, text);
+ BOOST_CHECK(load(testfile) == std::string(text));
+}
+
+// the representation has to match std::string's, so that an archive written
+// with one loads into the other
+static void test_interoperates(const char * testfile, const char * text){
+ save(testfile, text);
+ BOOST_CHECK(load(testfile) == std::string(text));
+
+ save(testfile, text);
+ BOOST_CHECK(load(testfile) == std::string(text));
+}
+
+#ifndef BOOST_NO_STD_WSTRING
+// the wide string borrows the std::wstring primitive the same way
+static void test_wide(const char * testfile, const wchar_t * text){
+ {
+ data d;
+ d.value = text;
+ d.tail = 42;
+ test_ostream os(testfile, TEST_STREAM_FLAGS);
+ test_oarchive oa(os, TEST_ARCHIVE_FLAGS);
+ oa << BOOST_SERIALIZATION_NVP(d);
+ }
+ data d;
+ d.tail = 0;
+ test_istream is(testfile, TEST_STREAM_FLAGS);
+ test_iarchive ia(is, TEST_ARCHIVE_FLAGS);
+ ia >> BOOST_SERIALIZATION_NVP(d);
+ BOOST_CHECK(42 == d.tail);
+ BOOST_CHECK(std::wstring(d.value.begin(), d.value.end())
+ == std::wstring(text));
+}
+#endif
+
+int
+test_main(int /* argc */, char * /* argv */ [])
+{
+ const char * testfile = boost::archive::tmpnam(NULL);
+ BOOST_REQUIRE(NULL != testfile);
+
+ const std::size_t n = sizeof(texts) / sizeof(texts[0]);
+ for(std::size_t i = 0; i < n; ++i){
+ test_roundtrip(testfile, texts[i]);
+ test_interoperates(testfile, texts[i]);
+ }
+
+ #ifndef BOOST_NO_STD_WSTRING
+ test_wide(testfile, L"D1");
+ test_wide(testfile, L"a b");
+ test_wide(testfile, L"");
+ #endif
+
+ std::remove(testfile);
+ return EXIT_SUCCESS;
+}