{"id":19719927,"url":"https://github.com/timboudreau/numble","last_synced_at":"2025-04-29T21:30:41.561Z","repository":{"id":22861387,"uuid":"26209046","full_name":"timboudreau/numble","owner":"timboudreau","description":"Generates typesafe, JSON-friendly Java classes with validation, from a description of input data as names and types","archived":false,"fork":false,"pushed_at":"2023-07-25T04:13:39.000Z","size":150,"stargazers_count":3,"open_issues_count":0,"forks_count":4,"subscribers_count":3,"default_branch":"master","last_synced_at":"2025-04-05T19:24:14.330Z","etag":null,"topics":["annotation","code-generation","jackson","json","pojo"],"latest_commit_sha":null,"homepage":null,"language":"Java","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/timboudreau.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null}},"created_at":"2014-11-05T07:49:46.000Z","updated_at":"2023-02-23T14:21:22.000Z","dependencies_parsed_at":"2023-01-13T22:20:53.742Z","dependency_job_id":null,"html_url":"https://github.com/timboudreau/numble","commit_stats":null,"previous_names":[],"tags_count":1,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/timboudreau%2Fnumble","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/timboudreau%2Fnumble/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/timboudreau%2Fnumble/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/timboudreau%2Fnumble/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/timboudreau","download_url":"https://codeload.github.com/timboudreau/numble/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":251585738,"owners_count":21613270,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["annotation","code-generation","jackson","json","pojo"],"created_at":"2024-11-11T23:09:43.953Z","updated_at":"2025-04-29T21:30:41.237Z","avatar_url":"https://github.com/timboudreau.png","language":"Java","funding_links":[],"categories":[],"sub_categories":[],"readme":"Numble\n======\n\nThis is a small framework-agnostic library that allows you to generate entity classes from an\nannotation, generate instances from data, and validate the input data they're\ncreated from.\n\nBasically, boilerplate getters and setters, marshalling and validation\ncode are no fun to write, and many frameworks force you to mix that sort of thing\ninto logic that does other things, which becomes hard to maintain.\n\nThis library lets you specify, declaratively, what your input looks like, and it\nwill generate a correctly implemented class to provide validation, marshalling\nand type-safe access to it.  It's common to deal in untyped data that consists\nof hashes of strings;  this library automatically generates a typesafe class for\nthat data.\n\nThe idea is to integrate this project into frameworks - so if you are, say,\nsupplying a constructor argument of a generated type, the framework validates\nthe data (and does the Right Thing\u0026trade; if the data is invalid) and then\nhands your code a beautiful, valid, typesafe object that represents the parameters\nyou expect.\n\nSo, you annotate a class like so:\n\n```java\n@Params(value = {\n    @Param(value = \"optionalSomething\", type = Types.NON_EMPTY_STRING, required = false),\n    @Param(value = \"requiredInt\", type = Types.INTEGER),\n    @Param(value = \"requiredBool\", type = Types.BOOLEAN, defaultValue = \"false\"),\n    @Param(value = \"requiredNonNeg\", type = Types.NON_NEGATIVE_INTEGER),\n    @Param(value = \"requiredNumber\", type = Types.DOUBLE, defaultValue = \"23\"),\n    @Param(value = \"nothing\", defaultValue = \"Go away\", required = false, \n           constraints = {StringValidators.MAY_NOT_END_WITH_PERIOD, \n                          StringValidators.MAY_NOT_START_WITH_DIGIT}),\n    @Param(value = \"defaultInt\", type = Types.INTEGER, defaultValue = \"5\"),\n    @Param(value = \"jthing\", type = Types.STRING, required = false, \n        validators = {LongerThanTwo.class, StartsWithJValidator.class})}\n        ,allowUnlistedParameters = true\n        ,generateToJSON = true\n        ,generateValidationCode = true)\n```\n\nand this generates, in the same package, an immutable class which has getters for all of\nthese parameters, using `Optional` for those properties which are not required and do\nnot have a default value.\n\nThe resulting class can be instantiated either via dependency injection (say, with Guice),\nor via deserialization using [Jackson](https://github.com/FasterXML/jackson) - it will have\none constructor with Jackson's annotations, and one annotated with `@Inject` which takes\nan instance of `KeysValues` (defined here - basically a map - bind it in your dependency\ninjection framework for whatever works for you - this keeps us framework-agnostic).\n\nThe goal was to create a framework for interpreting URL parameters and request bodies that\ncould be used with both [Acteur](https://timboudreau.com/blog/updatedActeur/read),\n[Wicket](https://wicket.apache.org/) and any similar framework that ingests key-value or\nJSON data.  For example, Acteur has the `@InjectRequestBodyAs` annotation, and soon\na similar one for URL parameters.  The goal was to eliminate manual parameter validation\nand type coercion code from classes whose job is the logic of handling a request, and\nwhich should be focused on that.\n\nThe generated class will be named `$NAME_OF_CLASS_WITH_THE_ANNOTATION + \"Params\"`.\n\n\nValidation\n----------\n\nThis project leverages a validation framework, [SimpleValidation](https://github.com/timboudreau/simplevalidation) (formerly https://kenai.com/projects/simplevalidation) - ([javadoc here](http://timboudreau.com/builds/job/SimpleValidation/lastSuccessfulBuild/artifact/ValidationAPI/target/apidocs/index.html))\nto validate input data.  Each parameter has two values that can list validators\n(a validator simply takes some input and either passes it or adds a localized error\nmessage to a list of problems):\n\n * `validators` - a list of validator classes.  They will be instantiated using Guice, so\nif they need to take some additional objects in their constructor, that is fine\n * `constraints` - a list of validators defined in the [StringValidators](http://timboudreau.com/builds/job/SimpleValidation/lastSuccessfulBuild/artifact/ValidationAPI/target/apidocs/org/netbeans/validation/api/builtin/stringvalidation/StringValidators.html) enum, which consists of a lot of predefined validators for URLs, email addreses and more.\n\nIt is preferable to perform validation *before* instantiating an object, so it\nis simply impossible for an object with invalid to exist.  However, generated objects\ncan be generated with a `validate()` method that will run validation post-hoc.\nThe `ParamChecker` class can be used to pre-validate data.\n\nUsage\n-----\n\nThe thing to remember is that you want to validate your data *before* you instantiate\nthe object, if at all possible.  So, typically, you know you are *going* to create\nan object from some data.  The class that can do pre-validation is `ParamChecker` - \ncreate one, create a [Problems](http://timboudreau.com/builds/job/SimpleValidation/lastSuccessfulBuild/artifact/ValidationAPI/target/apidocs/org/netbeans/validation/api/Problems.html)\nand pass your `KeysValues` to it first, if you're using injection.  If you're using\nJSON, an option is to load your data as a Map first, create a `KeysValues.MapAdapter` over\nthat.\n\nFailing that, you can instantiate your object using Jackson and (assuming `generateValidationCode() == true`)\ncall the `validate()` method of the resulting object to validate the object after-the-fact.\n\n#### Why Not Have Objects Throw An Exception In Their Constructor\n\nIt's generally not very nice to do that - in particular, Guice frowns upon that.\nSo, generally, validate your data *before* instantiating your object, and that\nway you're guaranteed never to have an instance of one of your types that is\nnot valid.\n\nIntegration with Acteur\n-----------------------\n\nTo use Numble with [Acteur](https://github.com/timboudreau/acteur), install the companion Guice module\n`ActeurNumbleModule`; then simply use Numble to create classes, and use them in, for example,\nthe `@InjectRequestBodyAs` annotation on your HTTP endpoints.  Validation failures will generate\n`400 Bad Request` responses with the validation errors included in the JSON body of the response,\ne.g.\n\n```json\n {\"error\":\"Invalid data\",\"problems\":[\"Port must be less than 65536\",\"INVALID_PORT\"]}\n```\n\nGetting The Library\n-------------------\n\nTo get it, add [the maven repository described here](http://timboudreau.com/builds) to your Maven\nproject, and then add a dependency\n\n```xml\n\u003cdependency\u003e\n    \u003cgroupId\u003ecom.mastfrog\u003c/groupId\u003e\n    \u003cartifactId\u003enumble\u003c/groupId\u003e\n    \u003cversion\u003e2.5.0-dev\u003c/groupId\u003e\n\u003c/dependency\u003e\n```\n\nGenerated Classes\n-----------------\n\nThe generated classes will have:\n\n - A constructor annotated with `@Inject` which takes a `KeysValues` (trivial to wrap a Map or similar in this)\n - Final fields + getters for all specified parameters, using `Optional` for those that could be null\n - Correct implementation of `equals()` and `hashCode()` and a meaningful `toString()` implementation\n - A `toMap()` method which converts the object to a `Map\u003cString,Object\u003e`\n - (optional) A constructor using Jackson's annotations for instantiation from JSON\n - (optional) A `toJSON()` method that converts the object back to JSON (if you do this directly, you'll need to\nconfigure Jackson to understand `Optional`, which it doesn't by default)\n - (optional) A `validate()` method for checking the correctness of that data post-instantiation\n - (optional) If `allowUnlistedParameters()` is true, the generated class will contain a `get(String)` method that returns `Optional\u003cString\u003e`, and an internal `__any(String,String)` method which will be annotated with `@JsonAnySetter` if `jsonConstructor()` returns true, so that properties that are not explicitly specified are captured\n\nNotes\n-----\n\nWhy not use [Lombok](http://projectlombok.org/) / other-random-library-like-this?\n\nI have fairly strong feelings about making data immutable, and the whole point here is\nthat you shouldn't have to manually define the class at all.  Plus, it was fun to\nwrite.\n\n\nGenerated Class Example\n-----------------------\n\nThis is what is generated from the annotations above:\n\n```java\n@Origin(com.mastfrog.parameters.FakePage.class)\npublic final class FakePageParams implements Serializable, Validatable {\n    private final int _defaultInt;\n    private final Optional\u003cString\u003e _jthing;\n    private final String _nothing;\n    private final Optional\u003cString\u003e _optionalSomething;\n    private final boolean _requiredBool;\n    private final int _requiredInt;\n    private final int _requiredNonNeg;\n    private final double _requiredNumber;\n    private final Map\u003cString,String\u003e __metadata = new HashMap\u003c\u003e();\n\n    @Inject\n    public FakePageParams (KeysValues params) {\n        this._defaultInt = params.get(\"defaultInt\") == null ? 5 : Integer.parseInt(params.get(\"defaultInt\"));\n        this._jthing = Optional.ofNullable(params.get(\"jthing\") == null ? null : params.get(\"jthing\"));\n        this._nothing = params.get(\"nothing\") == null ? \"Go away\" : params.get(\"nothing\");\n        this._optionalSomething = Optional.ofNullable(params.get(\"optionalSomething\") == null ? null : params.get(\"optionalSomething\"));\n        this._requiredBool = params.get(\"requiredBool\") == null ? false : Boolean.parseBoolean(params.get(\"requiredBool\"));\n        this._requiredInt = Integer.parseInt(params.get(\"requiredInt\"));\n        this._requiredNonNeg = Integer.parseInt(params.get(\"requiredNonNeg\"));\n        this._requiredNumber = params.get(\"requiredNumber\") == null ? 23 : Double.parseDouble(params.get(\"requiredNumber\"));\n        for (Map.Entry\u003cString,String\u003e __e : params) {\n            switch (__e.getKey()) {\n                case \"defaultInt\" :\n                case \"jthing\" :\n                case \"nothing\" :\n                case \"optionalSomething\" :\n                case \"requiredBool\" :\n                case \"requiredInt\" :\n                case \"requiredNonNeg\" :\n                case \"requiredNumber\" :\n                    break;\n                default :\n                    __any (__e.getKey(), __e.getValue());\n            }\n        }\n    }\n\n    @JsonCreator\n    public FakePageParams(\n        @JsonProperty(value=\"defaultInt\", required=false) Integer _defaultInt,\n        @JsonProperty(value=\"jthing\", required=false) String _jthing,\n        @JsonProperty(value=\"nothing\", required=false) String _nothing,\n        @JsonProperty(value=\"optionalSomething\", required=false) String _optionalSomething,\n        @JsonProperty(value=\"requiredBool\", required=false) Boolean _requiredBool,\n        @JsonProperty(value=\"requiredInt\") int _requiredInt,\n        @JsonProperty(value=\"requiredNonNeg\") int _requiredNonNeg,\n        @JsonProperty(value=\"requiredNumber\", required=false) Double _requiredNumber) {\n        this._defaultInt = _defaultInt == null ? 5 : _defaultInt;\n        this._jthing = Optional.ofNullable(_jthing);\n        this._nothing = _nothing == null ? \"Go away\" : _nothing;\n        this._optionalSomething = Optional.ofNullable(_optionalSomething);\n        this._requiredBool = _requiredBool == null ? false : _requiredBool;\n        this._requiredInt = _requiredInt;\n        this._requiredNonNeg = _requiredNonNeg;\n        this._requiredNumber = _requiredNumber == null ? 23 : _requiredNumber;\n    }\n\n\n    @JsonAnySetter\n    public void __any(String key, String value){\n        __metadata.put(key, value);\n    }\n\n    public Optional\u003cString\u003e get(String key) {\n        return Optional.ofNullable(__metadata.get(key));\n    }\n\n    public int getDefaultInt() {\n        return _defaultInt;\n    }\n\n    public Optional\u003cString\u003e getJthing() {\n        return _jthing;\n    }\n\n    public String getNothing() {\n        return _nothing;\n    }\n\n    public Optional\u003cString\u003e getOptionalSomething() {\n        return _optionalSomething;\n    }\n\n    public boolean getRequiredBool() {\n        return _requiredBool;\n    }\n\n    public int getRequiredInt() {\n        return _requiredInt;\n    }\n\n    public int getRequiredNonNeg() {\n        return _requiredNonNeg;\n    }\n\n    public double getRequiredNumber() {\n        return _requiredNumber;\n    }\n\n    @Override\n    public String toString() {\n        return \n           \" defaultInt = \" + _defaultInt\n            + \" jthing = \" + _jthing\n            + \" nothing = \" + _nothing\n            + \" optionalSomething = \" + _optionalSomething\n            + \" requiredBool = \" + _requiredBool\n            + \" requiredInt = \" + _requiredInt\n            + \" requiredNonNeg = \" + _requiredNonNeg\n            + \" requiredNumber = \" + _requiredNumber;\n    }\n\n    @Override\n    public boolean equals (Object o) {\n        if (o == this) {\n            return true;\n        } else if (o == null) {\n            return false;\n        }\n\n        if (o instanceof FakePageParams) {\n            FakePageParams other = (FakePageParams) o;\n            return \n                this._defaultInt == other._defaultInt \u0026\u0026\n                Objects.equals(this._jthing, other._jthing)  \u0026\u0026\n                Objects.equals(this._nothing, other._nothing)  \u0026\u0026\n                Objects.equals(this._optionalSomething, other._optionalSomething)  \u0026\u0026\n                Objects.equals(this._requiredBool, other._requiredBool)  \u0026\u0026\n                this._requiredInt == other._requiredInt \u0026\u0026\n                this._requiredNonNeg == other._requiredNonNeg \u0026\u0026\n                this._requiredNumber == other._requiredNumber;\n        }\n        return false;\n    }\n\n    @Override\n    public int hashCode() {\n        return Objects.hash(\n            _defaultInt,\n            _jthing,\n            _nothing,\n            _optionalSomething,\n            _requiredBool,\n            _requiredInt,\n            _requiredNonNeg,\n            _requiredNumber);\n    }\n\n    @Override\n    public Problems validate (Injector inj, Problems problems) {\n        if (_jthing.isPresent()) {\n            Validator\u003cString\u003e  _jthingValidator1 = inj.getInstance(com.mastfrog.parameters.LongerThanTwo.class);\n            _jthingValidator1.validate (problems, \"jthing\", _jthing.get() );\n        }\n        if (_jthing.isPresent()) {\n            Validator\u003cString\u003e  _jthingValidator2 = inj.getInstance(com.mastfrog.parameters.StartsWithJValidator.class);\n            _jthingValidator2.validate (problems, \"jthing\", _jthing.get() );\n        }\n        MAY_NOT_END_WITH_PERIOD.validate(problems, \"nothing\", _nothing);\n        MAY_NOT_START_WITH_DIGIT.validate(problems, \"nothing\", _nothing);\n        return problems;\n    }\n\n    public Map\u003cString,Object\u003e toMap() {\n        Map\u003cString,Object\u003e result = new HashMap\u003c\u003e();\n        result.put(\"defaultInt\", _defaultInt);\n        if (_jthing.isPresent()) {\n            result.put(\"jthing\", _jthing.get());\n        }\n        result.put(\"nothing\", _nothing);\n        if (_optionalSomething.isPresent()) {\n            result.put(\"optionalSomething\", _optionalSomething.get());\n        }\n        result.put(\"requiredBool\", _requiredBool);\n        result.put(\"requiredInt\", _requiredInt);\n        result.put(\"requiredNonNeg\", _requiredNonNeg);\n        result.put(\"requiredNumber\", _requiredNumber);\n        result.putAll(__metadata);\n        return result;\n    }\n\n    public String toJSON() throws JsonProcessingException {\n        return new ObjectMapper().writeValueAsString(toMap());\n    }\n}\n```\n\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Ftimboudreau%2Fnumble","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Ftimboudreau%2Fnumble","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Ftimboudreau%2Fnumble/lists"}