glideapps / quicktype

Generate types and converters from JSON, Schema, and GraphQL
https://app.quicktype.io
Apache License 2.0
11.79k stars 1.04k forks source link

[FEATURE]: better separation of types and serialization / deserialilzation code in generated C++ output #2628

Open beat-schaer opened 1 week ago

beat-schaer commented 1 week ago

I would like to use the generated C++ code in a Clean Architecture project. While the generated types itself are perfect candidates to end in the Entity Layer right in the core of the project, I want to put the serialization / deserialization code in one of the outer layers, preferably the Frameworks & Drivers Layer. However, that requires that the code is clearly separated according to these 2 different concerns.

Context (Input, Language)

Input Format: JSON Schema, draft-07 Output Language: C++

Description

The generated C++ content should be clearly separated without having any dependencies to the JSON serialization / deserialization code in the core types. Additionally it would be nice to also get an option of generating those 2 aspects of the code into separate directories (if using in multi-source mode. However, this is mandatory and can be easily fixed with a script after generation.

For the example used in this feature request, I used the following schema:

{
    "$schema": "http://json-schema.org/draft-07/schema#",
    "$ref": "#/definitions/TestObject",
    "definitions": {
        "TestObject": {
            "type": "object",
            "properties": {
                "some_string": {
                    "type": "string",
                    "pattern": "[0-9A-Za-z]+"
                }
            },
            "required": [ "some_string" ]
        }
    }
}

Current Behaviour / Output

This is what is currently generated, using Pascal case style for types and Underscore case for members.

// helper.hpp

//  To parse this JSON data, first install
//
//      json.hpp  https://github.com/nlohmann/json
//
//  Then include this file, and then do
//
//     helper.hpp data = nlohmann::json::parse(jsonString);

#pragma once

#include "json.hpp"

#include <optional>
#include <stdexcept>
#include <regex>

#include <sstream>

namespace data {
    using nlohmann::json;

    class ClassMemberConstraints {
        private:
        std::optional<int64_t> min_int_value;
        std::optional<int64_t> max_int_value;
        std::optional<double> min_double_value;
        std::optional<double> max_double_value;
        std::optional<size_t> min_length;
        std::optional<size_t> max_length;
        std::optional<std::string> pattern;

        public:
        ClassMemberConstraints(
            std::optional<int64_t> min_int_value,
            std::optional<int64_t> max_int_value,
            std::optional<double> min_double_value,
            std::optional<double> max_double_value,
            std::optional<size_t> min_length,
            std::optional<size_t> max_length,
            std::optional<std::string> pattern
        ) : min_int_value(min_int_value), max_int_value(max_int_value), min_double_value(min_double_value), max_double_value(max_double_value), min_length(min_length), max_length(max_length), pattern(pattern) {}
        ClassMemberConstraints() = default;
        virtual ~ClassMemberConstraints() = default;

        void set_min_int_value(int64_t min_int_value) { this->min_int_value = min_int_value; }
        auto get_min_int_value() const { return min_int_value; }

        void set_max_int_value(int64_t max_int_value) { this->max_int_value = max_int_value; }
        auto get_max_int_value() const { return max_int_value; }

        void set_min_double_value(double min_double_value) { this->min_double_value = min_double_value; }
        auto get_min_double_value() const { return min_double_value; }

        void set_max_double_value(double max_double_value) { this->max_double_value = max_double_value; }
        auto get_max_double_value() const { return max_double_value; }

        void set_min_length(size_t min_length) { this->min_length = min_length; }
        auto get_min_length() const { return min_length; }

        void set_max_length(size_t max_length) { this->max_length = max_length; }
        auto get_max_length() const { return max_length; }

        void set_pattern(const std::string &  pattern) { this->pattern = pattern; }
        auto get_pattern() const { return pattern; }
    };

    class ClassMemberConstraintException : public std::runtime_error {
        public:
        ClassMemberConstraintException(const std::string &  msg) : std::runtime_error(msg) {}
    };

    class ValueTooLowException : public ClassMemberConstraintException {
        public:
        ValueTooLowException(const std::string &  msg) : ClassMemberConstraintException(msg) {}
    };

    class ValueTooHighException : public ClassMemberConstraintException {
        public:
        ValueTooHighException(const std::string &  msg) : ClassMemberConstraintException(msg) {}
    };

    class ValueTooShortException : public ClassMemberConstraintException {
        public:
        ValueTooShortException(const std::string &  msg) : ClassMemberConstraintException(msg) {}
    };

    class ValueTooLongException : public ClassMemberConstraintException {
        public:
        ValueTooLongException(const std::string &  msg) : ClassMemberConstraintException(msg) {}
    };

    class InvalidPatternException : public ClassMemberConstraintException {
        public:
        InvalidPatternException(const std::string &  msg) : ClassMemberConstraintException(msg) {}
    };

    inline void CheckConstraint(const std::string &  name, const ClassMemberConstraints & c, int64_t value) {
        if (c.get_min_int_value() != std::nullopt && value < *c.get_min_int_value()) {
            throw ValueTooLowException ("Value too low for " + name + " (" + std::to_string(value) + "<" + std::to_string(*c.get_min_int_value()) + ")");
        }

        if (c.get_max_int_value() != std::nullopt && value > *c.get_max_int_value()) {
            throw ValueTooHighException ("Value too high for " + name + " (" + std::to_string(value) + ">" + std::to_string(*c.get_max_int_value()) + ")");
        }
    }

    inline void CheckConstraint(const std::string &  name, const ClassMemberConstraints & c, double value) {
        if (c.get_min_double_value() != std::nullopt && value < *c.get_min_double_value()) {
            throw ValueTooLowException ("Value too low for " + name + " (" + std::to_string(value) + "<" + std::to_string(*c.get_min_double_value()) + ")");
        }

        if (c.get_max_double_value() != std::nullopt && value > *c.get_max_double_value()) {
            throw ValueTooHighException ("Value too high for " + name + " (" + std::to_string(value) + ">" + std::to_string(*c.get_max_double_value()) + ")");
        }
    }

    inline void CheckConstraint(const std::string &  name, const ClassMemberConstraints & c, const std::string &  value) {
        if (c.get_min_length() != std::nullopt && value.length() < *c.get_min_length()) {
            throw ValueTooShortException ("Value too short for " + name + " (" + std::to_string(value.length()) + "<" + std::to_string(*c.get_min_length()) + ")");
        }

        if (c.get_max_length() != std::nullopt && value.length() > *c.get_max_length()) {
            throw ValueTooLongException ("Value too long for " + name + " (" + std::to_string(value.length()) + ">" + std::to_string(*c.get_max_length()) + ")");
        }

        if (c.get_pattern() != std::nullopt) {
            std::smatch result;
            std::regex_search(value, result, std::regex( *c.get_pattern() ));
            if (result.empty()) {
                throw InvalidPatternException ("Value doesn't match pattern for " + name + " (" + value +" != " + *c.get_pattern() + ")");
            }
        }
    }

    #ifndef NLOHMANN_UNTYPED_data_HELPER
    #define NLOHMANN_UNTYPED_data_HELPER
    inline json get_untyped(const json & j, const char * property) {
        if (j.find(property) != j.end()) {
            return j.at(property).get<json>();
        }
        return json();
    }

    inline json get_untyped(const json & j, std::string property) {
        return get_untyped(j, property.data());
    }
    #endif
}

// Generators.hpp

//  To parse this JSON data, first install
//
//      json.hpp  https://github.com/nlohmann/json
//
//  Then include this file, and then do
//
//     Generators.hpp data = nlohmann::json::parse(jsonString);

#pragma once

#include "json.hpp"
#include "helper.hpp"

#include "TestObject.hpp"

namespace data {
    void from_json(const json & j, TestObject & x);
    void to_json(json & j, const TestObject & x);

    inline void from_json(const json & j, TestObject& x) {
        x.set_some_string(j.at("some_string").get<std::string>());
    }

    inline void to_json(json & j, const TestObject & x) {
        j = json::object();
        j["some_string"] = x.get_some_string();
    }
}

// TestObject.hpp

//  To parse this JSON data, first install
//
//      json.hpp  https://github.com/nlohmann/json
//
//  Then include this file, and then do
//
//     TestObject.hpp data = nlohmann::json::parse(jsonString);

#pragma once

#include "json.hpp"
#include "helper.hpp"

namespace data {
    using nlohmann::json;

    class TestObject {
        public:
        TestObject() :
            some_string_constraint(std::nullopt, std::nullopt, std::nullopt, std::nullopt, std::nullopt, std::nullopt, std::string("[0-9A-Za-z]+"))
        {}
        virtual ~TestObject() = default;

        private:
        std::string some_string;
        ClassMemberConstraints some_string_constraint;

        public:
        const std::string & get_some_string() const { return some_string; }
        std::string & get_mutable_some_string() { return some_string; }
        void set_some_string(const std::string & value) { CheckConstraint("some_string", some_string_constraint, value); this->some_string = value; }
    };
}

// stdout

//  To parse this JSON data, first install
//
//      json.hpp  https://github.com/nlohmann/json
//
//  Then include this file, and then do
//
//     stdout data = nlohmann::json::parse(jsonString);

#pragma once

#include "json.hpp"
#include "helper.hpp"

#include "TestObject.hpp"
namespace data {
}

Proposed Behaviour / Output

This is my proposed new output. The following aspects should be changed from the current output:

// To parse this JSON data, first install // // json.hpp https://github.com/nlohmann/json // // Then include this file, and then do // // helper.hpp data = nlohmann::json::parse(jsonString);

pragma once

include "json.hpp"

namespace data {

#ifndef NLOHMANN_UNTYPED_data_HELPER
#define NLOHMANN_UNTYPED_data_HELPER
inline json get_untyped(const json & j, const char * property) {
    if (j.find(property) != j.end()) {
        return j.at(property).get<json>();
    }
    return json();
}

inline json get_untyped(const json & j, std::string property) {
    return get_untyped(j, property.data());
}
#endif

}

// ClassMemberContraints.hpp

pragma once

include

include

include

include

namespace data {

class ClassMemberConstraints {
    private:
    std::optional<int64_t> min_int_value;
    std::optional<int64_t> max_int_value;
    std::optional<double> min_double_value;
    std::optional<double> max_double_value;
    std::optional<size_t> min_length;
    std::optional<size_t> max_length;
    std::optional<std::string> pattern;

    public:
    ClassMemberConstraints(
        std::optional<int64_t> min_int_value,
        std::optional<int64_t> max_int_value,
        std::optional<double> min_double_value,
        std::optional<double> max_double_value,
        std::optional<size_t> min_length,
        std::optional<size_t> max_length,
        std::optional<std::string> pattern
    ) : min_int_value(min_int_value), max_int_value(max_int_value), min_double_value(min_double_value), max_double_value(max_double_value), min_length(min_length), max_length(max_length), pattern(pattern) {}
    ClassMemberConstraints() = default;
    virtual ~ClassMemberConstraints() = default;

    void set_min_int_value(int64_t min_int_value) { this->min_int_value = min_int_value; }
    auto get_min_int_value() const { return min_int_value; }

    void set_max_int_value(int64_t max_int_value) { this->max_int_value = max_int_value; }
    auto get_max_int_value() const { return max_int_value; }

    void set_min_double_value(double min_double_value) { this->min_double_value = min_double_value; }
    auto get_min_double_value() const { return min_double_value; }

    void set_max_double_value(double max_double_value) { this->max_double_value = max_double_value; }
    auto get_max_double_value() const { return max_double_value; }

    void set_min_length(size_t min_length) { this->min_length = min_length; }
    auto get_min_length() const { return min_length; }

    void set_max_length(size_t max_length) { this->max_length = max_length; }
    auto get_max_length() const { return max_length; }

    void set_pattern(const std::string &  pattern) { this->pattern = pattern; }
    auto get_pattern() const { return pattern; }
};

class ClassMemberConstraintException : public std::runtime_error {
    public:
    ClassMemberConstraintException(const std::string &  msg) : std::runtime_error(msg) {}
};

class ValueTooLowException : public ClassMemberConstraintException {
    public:
    ValueTooLowException(const std::string &  msg) : ClassMemberConstraintException(msg) {}
};

class ValueTooHighException : public ClassMemberConstraintException {
    public:
    ValueTooHighException(const std::string &  msg) : ClassMemberConstraintException(msg) {}
};

class ValueTooShortException : public ClassMemberConstraintException {
    public:
    ValueTooShortException(const std::string &  msg) : ClassMemberConstraintException(msg) {}
};

class ValueTooLongException : public ClassMemberConstraintException {
    public:
    ValueTooLongException(const std::string &  msg) : ClassMemberConstraintException(msg) {}
};

class InvalidPatternException : public ClassMemberConstraintException {
    public:
    InvalidPatternException(const std::string &  msg) : ClassMemberConstraintException(msg) {}
};

inline void CheckConstraint(const std::string &  name, const ClassMemberConstraints & c, int64_t value) {
    if (c.get_min_int_value() != std::nullopt && value < *c.get_min_int_value()) {
        throw ValueTooLowException ("Value too low for " + name + " (" + std::to_string(value) + "<" + std::to_string(*c.get_min_int_value()) + ")");
    }

    if (c.get_max_int_value() != std::nullopt && value > *c.get_max_int_value()) {
        throw ValueTooHighException ("Value too high for " + name + " (" + std::to_string(value) + ">" + std::to_string(*c.get_max_int_value()) + ")");
    }
}

inline void CheckConstraint(const std::string &  name, const ClassMemberConstraints & c, double value) {
    if (c.get_min_double_value() != std::nullopt && value < *c.get_min_double_value()) {
        throw ValueTooLowException ("Value too low for " + name + " (" + std::to_string(value) + "<" + std::to_string(*c.get_min_double_value()) + ")");
    }

    if (c.get_max_double_value() != std::nullopt && value > *c.get_max_double_value()) {
        throw ValueTooHighException ("Value too high for " + name + " (" + std::to_string(value) + ">" + std::to_string(*c.get_max_double_value()) + ")");
    }
}

inline void CheckConstraint(const std::string &  name, const ClassMemberConstraints & c, const std::string &  value) {
    if (c.get_min_length() != std::nullopt && value.length() < *c.get_min_length()) {
        throw ValueTooShortException ("Value too short for " + name + " (" + std::to_string(value.length()) + "<" + std::to_string(*c.get_min_length()) + ")");
    }

    if (c.get_max_length() != std::nullopt && value.length() > *c.get_max_length()) {
        throw ValueTooLongException ("Value too long for " + name + " (" + std::to_string(value.length()) + ">" + std::to_string(*c.get_max_length()) + ")");
    }

    if (c.get_pattern() != std::nullopt) {
        std::smatch result;
        std::regex_search(value, result, std::regex( *c.get_pattern() ));
        if (result.empty()) {
            throw InvalidPatternException ("Value doesn't match pattern for " + name + " (" + value +" != " + *c.get_pattern() + ")");
        }
    }
}

}

// Generators.hpp

// To parse this JSON data, first install // // json.hpp https://github.com/nlohmann/json // // Then include this file, and then do // // Generators.hpp data = nlohmann::json::parse(jsonString);

pragma once

include "json.hpp"

include "helper.hpp"

include "TestObject.hpp"

namespace data { void from_json(const json & j, TestObject & x); void to_json(json & j, const TestObject & x);

inline void from_json(const json & j, TestObject& x) {
    x.set_some_string(j.at("some_string").get<std::string>());
}

inline void to_json(json & j, const TestObject & x) {
    j = json::object();
    j["some_string"] = x.get_some_string();
}

}

// TestObject.hpp

pragma once

include "ClassMemberConstraints.hpp"

namespace data {

class TestObject {
    public:
    TestObject() :
        some_string_constraint(std::nullopt, std::nullopt, std::nullopt, std::nullopt, std::nullopt, std::nullopt, std::string("[0-9A-Za-z]+"))
    {}
    virtual ~TestObject() = default;

    private:
    std::string some_string;
    ClassMemberConstraints some_string_constraint;

    public:
    const std::string & get_some_string() const { return some_string; }
    std::string & get_mutable_some_string() { return some_string; }
    void set_some_string(const std::string & value) { CheckConstraint("some_string", some_string_constraint, value); this->some_string = value; }
};

}

// stdout

// To parse this JSON data, first install // // json.hpp https://github.com/nlohmann/json // // Then include this file, and then do // // stdout data = nlohmann::json::parse(jsonString);

pragma once

include "json.hpp"

include "helper.hpp"

include "Generators.hpp"

include "TestObject.hpp"

namespace data { }


## Solution

Either change the existing C++ generator or add a new option which allows this proposed separation.

Optionally add an option to already allow different directories, where the files with and without JSON-relation can be generated to.
## Alternatives

The alternative is to highly modify the generated code after the quicktype generation step. However, this is cumbersome and error-prone.

## Context