diff --git a/CHANGELOG.md b/CHANGELOG.md
index 57674776a27fea83cf3a475a3850b4545c6499fd..642983127050cfd2618bc88c49468d99aa7117b1 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1 +1,3 @@
 # v2.25.4
+
+* add JSONAbleDataObject
diff --git a/docs/json.rst b/docs/json.rst
index 3ef1e4858bc32a7c52bbd905ddf4ffc6102b2e82..937b50dab7755fc01c7d3d86080213324013e599 100644
--- a/docs/json.rst
+++ b/docs/json.rst
@@ -8,6 +8,9 @@ with
 .. autoclass:: satella.json.JSONAble
     :members:
 
+.. autoclass:: satella.json.JSONAbleDataObject
+    :members:
+
 Then you can convert structures made out of standard serializable Python JSON objects, such as dicts
 and lists, and also JSONAble objects, by this all
 
diff --git a/satella/__init__.py b/satella/__init__.py
index 53acef843327abced8aea829340a4656f9ea1ed8..4061a19a0e827b308381ce83b72e8ff62d827d93 100644
--- a/satella/__init__.py
+++ b/satella/__init__.py
@@ -1 +1 @@
-__version__ = '2.25.4a1'
+__version__ = '2.25.4a2'
diff --git a/satella/json.py b/satella/json.py
index 073412df2b619a4fc6c6fe7f07935d9f438a5009..6c03ea5fbec5e625f87996c2abfd62a47eb69d97 100644
--- a/satella/json.py
+++ b/satella/json.py
@@ -1,3 +1,5 @@
+from __future__ import annotations
+
 import enum
 import json
 import typing as tp
@@ -8,7 +10,8 @@ from satella.coding.typing import NoneType
 from satella.files import write_out_file_if_different
 
 __all__ = ['JSONEncoder', 'JSONAble', 'json_encode', 'read_json_from_file',
-           'write_json_to_file', 'write_json_to_file_if_different']
+           'write_json_to_file', 'write_json_to_file_if_different', 'JSONAbleDataObject']
+
 
 Jsonable = tp.TypeVar('Jsonable', list, dict, str, int, float, None)
 
@@ -119,3 +122,57 @@ def read_json_from_file(path: str) -> JSONAble:
             except json.decoder.JSONDecodeError as e:
                 raise ValueError(str(e))
     return v
+
+
+class JSONAbleDataObject:
+    """
+    A data-class that supports conversion of it's classes to JSON
+
+    Define like this:
+
+    >>> class CultureContext(JSONAbleDataObject):
+    >>>     language: str
+    >>>     timezone: str
+    >>>     units: str = 'metric'
+
+    Note that type annotation is mandatory and default values are supported. Being data value objects, these are
+    eq-able and hashable.
+
+    And use like this:
+
+    >>> a = CultureContext(language='pl', timezone='Europe/Warsaw')
+    >>> assert a.to_json() == {'language': 'pl', 'timezone': 'Europe/Warsaw', 'units': 'metric'}
+    >>> assert CultureContext.from_json(a.to_json) == a
+    """
+
+    def __eq__(self, other) -> bool:
+        return all(getattr(self, annotation) == getattr(other, annotation) for annotation in self.__class__.__annotations__.keys())
+
+    def __hash__(self) -> int:
+        hash_ = 0
+        for annotation in self.__class__.__annotations__.keys():
+            hash_ ^= hash(getattr(self, annotation))
+        return hash_
+
+    def __init__(self, **kwargs):
+        """
+        :raises ValueError: a non-default value was not provided
+        """
+        for annotation in self.__class__.__annotations__.keys():
+            try:
+                annot_val = kwargs.pop(annotation)
+                setattr(self, annotation, annot_val)
+            except KeyError:
+                if not hasattr(self, annotation):
+                    raise ValueError(f'Argument {annotation} not provided!')
+
+    def to_json(self) -> tp.Dict:
+        """Convert self to JSONable value"""
+        result = {}
+        for annotation in self.__class__.__annotations__.keys():
+            result[annotation] = getattr(self, annotation)
+        return result
+
+    @classmethod
+    def from_json(cls, jsonable) -> JSONAbleDataObject:
+        return cls(**jsonable)
diff --git a/tests/test_coding/test_structures.py b/tests/test_coding/test_structures.py
index 40526c1326d1a2b3464ae9102d106b3613601dce..f33ff62f366ed58c8be0ec054554021347b98218 100644
--- a/tests/test_coding/test_structures.py
+++ b/tests/test_coding/test_structures.py
@@ -15,7 +15,7 @@ from satella.coding.structures import TimeBasedHeap, Heap, typednamedtuple, \
     CacheDict, StrEqHashableMixin, ComparableIntEnum, HashableIntEnum, ComparableAndHashableBy, \
     ComparableAndHashableByInt, SparseMatrix, ExclusiveWritebackCache, Subqueue, \
     CountingDict, ComparableEnum, LRU, LRUCacheDict, Vector, DefaultDict, PushIterable, \
-    ComparableAndHashableByStr, NotEqualToAnything, NOT_EQUAL_TO_ANYTHING, DictionaryEQAble, SetZip, OnStrOnlyName
+    ComparableAndHashableByStr, NotEqualToAnything, NOT_EQUAL_TO_ANYTHING, DictionaryEQAble, SetZip, OnStrOnlyNameJSONAbleDataObject
 
 
 def continue_testing_omni(self, omni_class):
diff --git a/tests/test_json.py b/tests/test_json.py
index 0294141df6c8a73042ec52505593e3060578b282..2c71027d11d0bfe3d01eea23f4e81eb24a506851 100644
--- a/tests/test_json.py
+++ b/tests/test_json.py
@@ -4,11 +4,22 @@ import typing as tp
 import unittest
 
 from satella.json import JSONAble, json_encode, read_json_from_file, write_json_to_file, \
-    write_json_to_file_if_different, JSONEncoder
+    write_json_to_file_if_different, JSONEncoder, JSONAbleDataObject
 
 
 class TestJson(unittest.TestCase):
 
+    def test_jsonable_data_object(self):
+        class CultureContext(JSONAbleDataObject):
+            units: str = 'metric'
+            timezone: str
+            language: str
+
+        a = CultureContext(language='pl', timezone='Europe/Warsaw')
+        self.assertEquals(a.to_json(), {'language': 'pl', 'timezone': 'Europe/Warsaw', 'units': 'metric'})
+        self.assertEquals(CultureContext.from_json(a.to_json()), a)
+        self.assertEquals(hash(CultureContext.from_json(a.to_json())), hash(a))
+
     def test_json_encoder_enums(self):
         enc = JSONEncoder()