Repository: cwacek/python-jsonschema-objects

Issue:
Request of the const field support
**What are you trying to do?**
We use this library to automatically generate classes to validate our data format.

**Example Schema and code**
```
import python_jsonschema_objects as pjs
exclass = {
    "type": "object",
    "properties": {
        "region_type": {  "const": "RECTANGLE"  }
    },
    "title": "Example"
}
builder = pjs.ObjectBuilder(exclass)
available_classes = builder.build_classes()
```
Error: "NotImplementedError: Unable to parse schema object '{'const': 'RECTANGLE', 'raw_name': 'region_type'}' with no type and no reference"

**Is this a currently unsupported jsonschema directive? If so, please link to the documentation**

I'd like to be able to use the format validator from v7, as described here: https://json-schema.org/understanding-json-schema/reference/generic.html#constant-values

**Do you have an idea about how this should work?**

Support of "const" field in schema could be realized in a way similar to "enum".
```
"region_type": { "enum": [ "RECTANGLE" ] }
```
Last format in my json schema works.

Discussion:

Patch:
diff --git a/python_jsonschema_objects/classbuilder.py b/python_jsonschema_objects/classbuilder.py
index e9aaabe..783b064 100644
--- a/python_jsonschema_objects/classbuilder.py
+++ b/python_jsonschema_objects/classbuilder.py
@@ -4,6 +4,7 @@ import itertools
 import logging
 import sys
 
+import jsonschema.exceptions
 import referencing._core
 import six
 
@@ -184,11 +185,23 @@ class ProtocolBase(collections.abc.MutableMapping):
         # but only for the ones that have defaults set.
         for name in self.__has_default__:
             if name not in props:
-                default_value = copy.deepcopy(self.__propinfo__[name]["default"])
+                # "defaults" could come from either the 'default' keyword or the 'const' keyword
+                try:
+                    default_value = self.__propinfo__[name]["default"]
+                except KeyError:
+                    try:
+                        default_value = self.__propinfo__[name]["const"]
+                    except KeyError:
+                        raise jsonschema.exceptions.SchemaError(
+                            "Schema parsing error. Expected {0} to have default or const value".format(
+                                name
+                            )
+                        )
+
                 logger.debug(
                     util.lazy_format("Initializing '{0}' to '{1}'", name, default_value)
                 )
-                setattr(self, name, default_value)
+                setattr(self, name, copy.deepcopy(default_value))
 
         for prop in props:
             try:
@@ -626,7 +639,7 @@ class ClassBuilder(object):
                 "__propinfo__": {
                     "__literal__": clsdata,
                     "__title__": clsdata.get("title"),
-                    "__default__": clsdata.get("default"),
+                    "__default__": clsdata.get("default") or clsdata.get("const"),
                 }
             },
         )
@@ -670,6 +683,17 @@ class ClassBuilder(object):
                 )
                 defaults.add(prop)
 
+            if "const" in detail:
+                logger.debug(
+                    util.lazy_format(
+                        "Setting const for {0}.{1} to: {2}",
+                        nm,
+                        prop,
+                        detail["const"],
+                    )
+                )
+                defaults.add(prop)
+
             if detail.get("type", None) == "object":
                 uri = "{0}/{1}_{2}".format(nm, prop, "<anonymous>")
                 self.resolved[uri] = self.construct(uri, detail, (ProtocolBase,), **kw)
diff --git a/python_jsonschema_objects/literals.py b/python_jsonschema_objects/literals.py
index d56c946..37367e1 100644
--- a/python_jsonschema_objects/literals.py
+++ b/python_jsonschema_objects/literals.py
@@ -44,6 +44,10 @@ class LiteralValue(object):
 
         self.validate()
 
+        constval = self.const()
+        if constval is not None:
+            self._value = constval
+
     def as_dict(self):
         return self.for_json()
 
@@ -54,6 +58,10 @@ class LiteralValue(object):
     def default(cls):
         return cls.__propinfo__.get("__default__")
 
+    @classmethod
+    def const(cls):
+        return cls.__propinfo__.get("__literal__", {}).get("const", None)
+
     @classmethod
     def propinfo(cls, propname):
         if propname not in cls.__propinfo__:
diff --git a/python_jsonschema_objects/validators.py b/python_jsonschema_objects/validators.py
index 92a792d..b2921a2 100644
--- a/python_jsonschema_objects/validators.py
+++ b/python_jsonschema_objects/validators.py
@@ -58,6 +58,12 @@ def enum(param, value, _):
         raise ValidationError("{0} is not one of {1}".format(value, param))
 
 
+@registry.register()
+def const(param, value, _):
+    if value != param:
+        raise ValidationError("{0} is not constant {1}".format(value, param))
+
+
 @registry.register()
 def minimum(param, value, type_data):
     exclusive = type_data.get("exclusiveMinimum")


Test patch:
diff --git a/test/test_229.py b/test/test_229.py
new file mode 100644
index 0000000..cb8d36a
--- /dev/null
+++ b/test/test_229.py
@@ -0,0 +1,51 @@
+import pytest
+
+import python_jsonschema_objects as pjo
+
+
+def test_const_properties():
+    schema = {
+        "title": "Example",
+        "type": "object",
+        "properties": {
+            "url": {
+                "type": "string",
+                "default": "https://example.com/your-username/my-project",
+            },
+            "type": {"type": "string", "const": "git"},
+        },
+    }
+
+    ob = pjo.ObjectBuilder(schema)
+    ns1 = ob.build_classes()
+    ex = ns1.Example()
+    ex.url = "can be anything"
+
+    # we expect the value to be set already for const values
+    assert ex.type == "git"
+    with pytest.raises(pjo.ValidationError):
+        # Trying to set the value to something else should throw validation errors
+        ex.type = "mercurial"
+
+    # setting the value to the const value is a no-op, but permitted
+    ex.type = "git"
+
+
+def test_const_bare_type():
+    schema = {
+        "title": "Example",
+        "type": "string",
+        "const": "I stand alone",
+    }
+
+    ob = pjo.ObjectBuilder(schema)
+    ns1 = ob.build_classes()
+    ex = ns1.Example("I stand alone")
+    # we expect the value to be set already for const values
+    assert ex == "I stand alone"
+    with pytest.raises(pjo.ValidationError):
+        # Trying to set the value to something else should throw validation errors
+        ex = ns1.Example("mercurial")
+
+    # setting the value to the const value is a no-op, but permitted
+    ex = ns1.Example("I stand alone")
diff --git a/test/test_default_values.py b/test/test_default_values.py
index dc76ec5..3c0743f 100644
--- a/test/test_default_values.py
+++ b/test/test_default_values.py
@@ -53,15 +53,17 @@ def test_nullable_types_are_still_nullable(ns):
 
     thing1.p1 = None
     thing1.validate()
-    assert thing1.as_dict() == {"p1": 0, "p2": None}
+    assert thing1.as_dict() == {"p1": None, "p2": None}
 
 
 def test_null_types_without_defaults_do_not_serialize(ns):
     thing1 = ns.DefaultTest()
 
+    assert thing1.as_dict() == {"p1": 0, "p2": None}
+
     thing1.p3 = 10
     thing1.validate()
     thing1.p1 = None
     thing1.validate()
 
-    assert thing1.as_dict() == {"p1": 0, "p2": None, "p3": 10}
+    assert thing1.as_dict() == {"p1": None, "p2": None, "p3": 10}